Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.

Commit 0c7e4f6

Browse files
authored
feat: add x-goog-api-client headers for retry metrics (#1920)
* feat: add x-goog-api-client headers for retry metrics * update tests, add invocation id to more places * add invocation id and tests to gcs resumable upload * fix event naming * fix method of pulling package.json, update regex
1 parent 850733c commit 0c7e4f6

7 files changed

Lines changed: 210 additions & 54 deletions

File tree

src/gcs-resumable-upload.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,20 @@ import {GoogleAuth, GoogleAuthOptions} from 'google-auth-library';
2727
import {Readable, Writable} from 'stream';
2828
import retry = require('async-retry');
2929
import {RetryOptions, PreconditionOptions} from './storage';
30+
import * as uuid from 'uuid';
3031

3132
const NOT_FOUND_STATUS_CODE = 404;
3233
const TERMINATED_UPLOAD_STATUS_CODE = 410;
3334
const RESUMABLE_INCOMPLETE_STATUS_CODE = 308;
3435
const DEFAULT_API_ENDPOINT_REGEX = /.*\.googleapis\.com/;
36+
let packageJson: ReturnType<JSON['parse']> = {};
37+
try {
38+
// if requiring from 'build' (default)
39+
packageJson = require('../../package.json');
40+
} catch (e) {
41+
// if requiring directly from TypeScript context
42+
packageJson = require('../package.json');
43+
}
3544

3645
export const PROTOCOL_REGEX = /^(\w*):\/\//;
3746

@@ -262,6 +271,11 @@ export class Upload extends Writable {
262271
contentLength: number | '*';
263272
retryOptions: RetryOptions;
264273
timeOfFirstRequest: number;
274+
private currentInvocationId = {
275+
chunk: uuid.v4(),
276+
uri: uuid.v4(),
277+
offset: uuid.v4(),
278+
};
265279
private upstreamChunkBuffer: Buffer = Buffer.alloc(0);
266280
private chunkBufferEncoding?: BufferEncoding = undefined;
267281
private numChunksReadInRequest = 0;
@@ -530,6 +544,7 @@ export class Upload extends Writable {
530544
protected async createURIAsync(): Promise<string> {
531545
const metadata = this.metadata;
532546

547+
// Check if headers already exist before creating new ones
533548
const reqOpts: GaxiosOptions = {
534549
method: 'POST',
535550
url: [this.baseURI, this.bucket, 'o'].join('/'),
@@ -541,7 +556,9 @@ export class Upload extends Writable {
541556
this.params
542557
),
543558
data: metadata,
544-
headers: {},
559+
headers: {
560+
'x-goog-api-client': `gl-node/${process.versions.node} gccl/${packageJson.version} gccl-invocation-id/${this.currentInvocationId.uri}`,
561+
},
545562
};
546563

547564
if (metadata.contentLength) {
@@ -572,6 +589,8 @@ export class Upload extends Writable {
572589
async (bail: (err: Error) => void) => {
573590
try {
574591
const res = await this.makeRequest(reqOpts);
592+
// We have successfully got a URI we can now create a new invocation id
593+
this.currentInvocationId.uri = uuid.v4();
575594
return res.headers.location;
576595
} catch (err) {
577596
const e = err as GaxiosError;
@@ -707,20 +726,20 @@ export class Upload extends Writable {
707726
},
708727
});
709728

710-
let headers: GaxiosOptions['headers'] = {};
729+
const headers: GaxiosOptions['headers'] = {
730+
'x-goog-api-client': `gl-node/${process.versions.node} gccl/${packageJson.version} gccl-invocation-id/${this.currentInvocationId.chunk}`,
731+
};
711732

712733
// If using multiple chunk upload, set appropriate header
713734
if (multiChunkMode && expectedUploadSize) {
714735
// The '-1' is because the ending byte is inclusive in the request.
715736
const endingByte = expectedUploadSize + this.numBytesWritten - 1;
716-
headers = {
717-
'Content-Length': expectedUploadSize,
718-
'Content-Range': `bytes ${this.offset}-${endingByte}/${this.contentLength}`,
719-
};
737+
headers['Content-Length'] = expectedUploadSize;
738+
headers[
739+
'Content-Range'
740+
] = `bytes ${this.offset}-${endingByte}/${this.contentLength}`;
720741
} else {
721-
headers = {
722-
'Content-Range': `bytes ${this.offset}-*/${this.contentLength}`,
723-
};
742+
headers['Content-Range'] = `bytes ${this.offset}-*/${this.contentLength}`;
724743
}
725744

726745
const reqOpts: GaxiosOptions = {
@@ -750,6 +769,9 @@ export class Upload extends Writable {
750769
return;
751770
}
752771

772+
// At this point we can safely create a new id for the chunk
773+
this.currentInvocationId.chunk = uuid.v4();
774+
753775
const shouldContinueWithNextMultiChunkRequest =
754776
this.chunkSize &&
755777
resp.status === RESUMABLE_INCOMPLETE_STATUS_CODE &&
@@ -849,10 +871,16 @@ export class Upload extends Writable {
849871
const opts: GaxiosOptions = {
850872
method: 'PUT',
851873
url: this.uri!,
852-
headers: {'Content-Length': 0, 'Content-Range': 'bytes */*'},
874+
headers: {
875+
'Content-Length': 0,
876+
'Content-Range': 'bytes */*',
877+
'x-goog-api-client': `gl-node/${process.versions.node} gccl/${packageJson.version} gccl-invocation-id/${this.currentInvocationId.offset}`,
878+
},
853879
};
854880
try {
855881
const resp = await this.makeRequest(opts);
882+
// Successfully got the offset we can now create a new offset invocation id
883+
this.currentInvocationId.offset = uuid.v4();
856884
if (resp.status === RESUMABLE_INCOMPLETE_STATUS_CODE) {
857885
if (resp.headers.range) {
858886
const range = resp.headers.range as string;

src/nodejs-common/service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import arrify = require('arrify');
1717
import * as extend from 'extend';
1818
import {AuthClient, GoogleAuth, GoogleAuthOptions} from 'google-auth-library';
1919
import * as r from 'teeny-request';
20+
import * as uuid from 'uuid';
2021

2122
import {Interceptor} from './service-object';
2223
import {
@@ -242,7 +243,9 @@ export class Service {
242243
}
243244
reqOpts.headers = extend({}, reqOpts.headers, {
244245
'User-Agent': userAgent,
245-
'x-goog-api-client': `gl-node/${process.versions.node} gccl/${pkg.version}`,
246+
'x-goog-api-client': `gl-node/${process.versions.node} gccl/${
247+
pkg.version
248+
} gccl-invocation-id/${uuid.v4()}`,
246249
});
247250

248251
if (reqOpts.shouldReturnStream) {

src/nodejs-common/util.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ import * as r from 'teeny-request';
2727
import * as retryRequest from 'retry-request';
2828
import {Duplex, DuplexOptions, Readable, Transform, Writable} from 'stream';
2929
import {teenyRequest} from 'teeny-request';
30-
3130
import {Interceptor} from './service-object';
31+
import * as uuid from 'uuid';
32+
import * as packageJson from '../../package.json';
3233

3334
// eslint-disable-next-line @typescript-eslint/no-var-requires
3435
const duplexify: DuplexifyConstructor = require('duplexify');
3536

36-
const requestDefaults = {
37+
const requestDefaults: r.CoreOptions = {
3738
timeout: 60000,
3839
gzip: true,
3940
forever: true,
@@ -522,6 +523,7 @@ export class Util {
522523
return;
523524
}
524525

526+
requestDefaults.headers = util._getDefaultHeaders();
525527
const request = teenyRequest.defaults(requestDefaults);
526528
request(authenticatedReqOpts!, (err, resp, body) => {
527529
util.handleResp(err, resp, body, (err, data) => {
@@ -797,6 +799,7 @@ export class Util {
797799
maxRetryValue = config.retryOptions.maxRetries;
798800
}
799801

802+
requestDefaults.headers = this._getDefaultHeaders();
800803
const options = {
801804
request: teenyRequest.defaults(requestDefaults),
802805
retries: autoRetryValue !== false ? maxRetryValue : 0,
@@ -945,6 +948,15 @@ export class Util {
945948
? [{} as T, optionsOrCallback as C]
946949
: [optionsOrCallback as T, cb as C];
947950
}
951+
952+
_getDefaultHeaders() {
953+
return {
954+
'User-Agent': util.getUserAgentFromPackageJson(packageJson),
955+
'x-goog-api-client': `gl-node/${process.versions.node} gccl/${
956+
packageJson.version
957+
} gccl-invocation-id/${uuid.v4()}`,
958+
};
959+
}
948960
}
949961

950962
/**

0 commit comments

Comments
 (0)