From 73a5d8b5af649a4d014c1021c0c8f87cfa344a4f Mon Sep 17 00:00:00 2001 From: KhoaHTD Date: Sat, 11 Nov 2023 13:18:01 +0700 Subject: [PATCH] feat: add Logging for summary and detail --- jest.config.js | 1 + package.json | 1 + src/lib/LoggingInterceptor.ts | 185 ++++++++++++++++++++++++++++++++++ src/lib/Service.ts | 8 +- src/lib/constants.ts | 2 + src/lib/types.ts | 12 ++- yarn.lock | 58 ++++++++++- 7 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 src/lib/LoggingInterceptor.ts diff --git a/jest.config.js b/jest.config.js index f2b26c3..4bb7eb1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,7 @@ module.exports = { '/build/', '/src/generated/', '/src/tests/', + '/src/lib/LoggingInterceptor.ts', 'jest.config.js', ], testPathIgnorePatterns: ['/node_modules/', '/build/'], diff --git a/package.json b/package.json index c3c4823..0947002 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "dependencies": { "@grpc/grpc-js": "^1.8.5", "deepmerge": "^4.2.2", + "log4js": "^6.9.1", "protobufjs": "^7.1.2" }, "devDependencies": { diff --git a/src/lib/LoggingInterceptor.ts b/src/lib/LoggingInterceptor.ts new file mode 100644 index 0000000..d7151ac --- /dev/null +++ b/src/lib/LoggingInterceptor.ts @@ -0,0 +1,185 @@ +import { + InterceptingCall, + InterceptorOptions, + Metadata, + Interceptor as GRPCInterceptor, + StatusObject, +} from '@grpc/grpc-js'; +import { Status } from '@grpc/grpc-js/build/src/constants'; +import { + FullRequester, + ListenerBuilder, + NextCall, + RequesterBuilder, +} from '@grpc/grpc-js/build/src/client-interceptors'; +import log4js from 'log4js'; +import type { Logger } from 'log4js'; +import { Interceptor, LoggingOptions } from './types'; +import { HOST } from './constants'; + +const cleanEmpty = function (obj: any, defaults = [undefined, null]): any { + if (defaults.includes(obj)) return; + + if (Array.isArray(obj)) + return obj + .map((v) => (v && typeof v === 'object' ? cleanEmpty(v, defaults) : v)) + .filter((v) => !defaults.includes(v)); + + return Object.entries(obj).length + ? Object.entries(obj) + .map(([k, v]) => [ + k, + v && typeof v === 'object' ? cleanEmpty(v, defaults) : v, + ]) + .reduce( + (a, [k, v]) => (defaults.includes(v) ? a : { ...a, [k]: v }), + {}, + ) + : obj; +}; + +export class LoggingInterceptor implements Interceptor { + private requestLogging: boolean | LoggingOptions; + private summaryLogger: Logger; + private detailLogger: Logger; + + constructor(requestLogging: boolean | LoggingOptions) { + this.requestLogging = requestLogging; + + log4js.configure({ + appenders: { + out: { type: 'stdout' }, + }, + categories: { + default: { appenders: ['out'], level: 'info' }, + }, + }); + this.summaryLogger = log4js.getLogger('Google::Ads::GoogleAds::Summary'); + this.detailLogger = log4js.getLogger('Google::Ads::GoogleAds::Detail'); + } + + private logSummary( + responseStatus: StatusObject, + request: any, + options: InterceptorOptions, + responseHeaders: Metadata, + ) { + if ( + this.requestLogging === true || + (this.requestLogging).summary === true + ) { + const isSuccess = responseStatus.code == Status.OK.valueOf(); + + const messages = [ + `${isSuccess ? 'SUCCESS' : 'FAILURE'} REQUEST SUMMARY.`, + `Host=${HOST}`, + `Method=${options.method_definition.path}`, + `ClientCustomerId=${request.customer_id}`, + `RequestId=${responseHeaders.get('request-id')}`, + `ResponseCode=${responseStatus.code}`, + ]; + + if (isSuccess) { + this.summaryLogger.info(messages.join(' ')); + } else { + messages.push(`Fault=${responseStatus.details}`); + this.summaryLogger.warn(messages.join(' ')); + } + } + } + + private logDetail( + responseStatus: StatusObject, + request: any, + requestHeaders: Metadata, + options: InterceptorOptions, + response: any, + responseHeaders: Metadata, + ) { + if ( + this.requestLogging === true || + (this.requestLogging).detail === true + ) { + const isSuccess = responseStatus.code == Status.OK.valueOf(); + + const messages = [ + `${isSuccess ? 'SUCCESS' : 'FAILURE'} REQUEST DETAIL.`, + 'Request', + '-------', + `MethodName: ${options.method_definition.path}`, + `Host: ${HOST}`, + `Headers: ${JSON.stringify(requestHeaders.getMap())}`, + `Body: ${JSON.stringify(request)}`, + `\nResponse`, + '--------', + `Headers: ${JSON.stringify(responseHeaders.getMap())}`, + `Body: ${JSON.stringify(cleanEmpty(response))}`, + `ResponseCode: ${responseStatus.code}`, + ]; + + if (isSuccess) { + this.detailLogger.debug(messages.join('\n')); + } else { + messages.push(`Fault: ${responseStatus.details}`); + this.detailLogger.info(messages.join('\n')); + } + } + } + + interceptCall: GRPCInterceptor = ( + options: InterceptorOptions, + nextCall: NextCall, + ) => { + let request: any; + let requestHeaders: Metadata; + let response: any; + let responseHeaders: Metadata; + + const requester: Partial = new RequesterBuilder() + .withStart((headers, responseListener, next) => { + requestHeaders = headers; + + const listener = new ListenerBuilder() + .withOnReceiveMessage((message, next) => { + response = message; + next(message); + }) + .withOnReceiveMetadata((metadata, next) => { + responseHeaders = metadata; + next(metadata); + }) + .withOnReceiveStatus((status, next) => { + try { + this.logSummary(status, request, options, responseHeaders); + this.logDetail( + status, + request, + requestHeaders, + options, + response, + responseHeaders, + ); + } catch (error) { + } finally { + next(status); + } + }) + .build(); + + next(headers, listener); + }) + .withSendMessage((message, next) => { + request = message; + next(message); + }) + .withHalfClose((next) => { + next(); + }) + .withCancel((next) => { + next(); + }) + .build(); + + return new InterceptingCall(nextCall(options), requester); + }; +} diff --git a/src/lib/Service.ts b/src/lib/Service.ts index dcfbfa5..7a9d196 100644 --- a/src/lib/Service.ts +++ b/src/lib/Service.ts @@ -3,6 +3,8 @@ import { Metadata } from '@grpc/grpc-js'; import { ServiceProvider } from './ServiceProvider'; import { AllServices, ServiceName, ServiceOptions } from './types'; import { getCredentials } from './utils'; +import { LoggingInterceptor } from './LoggingInterceptor'; +import { HOST } from './constants'; export class Service extends ServiceProvider { // @ts-expect-error All fields don't need to be set here @@ -28,7 +30,11 @@ export class Service extends ServiceProvider { const credentials = getCredentials(this.options.auth); - const client = new ProtoService('googleads.googleapis.com', credentials); + const client = new ProtoService(HOST, credentials, { + interceptors: [ + new LoggingInterceptor(this.options.logging || false).interceptCall, + ], + }); this.cachedClients[serviceName] = client; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index b26b7a1..3764a14 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,3 +1,5 @@ +export const HOST = 'googleads.googleapis.com' as const; + export const VERSION = 'v13' as const; export const FAILURE_KEY = `google.ads.googleads.${VERSION}.errors.googleadsfailure-bin`; diff --git a/src/lib/types.ts b/src/lib/types.ts index 462135b..3488fb7 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,4 @@ -import { OAuth2Client } from '@grpc/grpc-js'; +import { Interceptor as GRPCInterceptor, OAuth2Client } from '@grpc/grpc-js'; import allProtos from '../generated/google'; import { VERSION } from './constants'; @@ -8,9 +8,15 @@ export type OptionalExceptFor = Partial & export type AllServices = Omit; export type ServiceName = keyof Omit; +export type LoggingOptions = { + summary?: boolean; + detail?: boolean; +}; + export interface ServiceOptions { auth: OAuth2Client; developer_token: string; + logging?: boolean | LoggingOptions; } export interface CustomerOptions { @@ -80,3 +86,7 @@ export interface OrderBy { attribute: string; direction?: OrderDirection; } + +export interface Interceptor { + interceptCall: GRPCInterceptor; +} diff --git a/yarn.lock b/yarn.lock index 95a13d6..efd13c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1198,7 +1198,12 @@ dataloader@^1.4.0: resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw== -debug@4, debug@^4.1.0, debug@^4.1.1: +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -1376,6 +1381,11 @@ find-yarn-workspace-root@^2.0.0: dependencies: micromatch "^4.0.2" +flatted@^3.2.7: + version "3.2.9" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" + integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== + form-data@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -1385,6 +1395,15 @@ form-data@4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^9.0.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -2108,6 +2127,13 @@ json5@^2.2.2, json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -2183,6 +2209,17 @@ log-driver@1.2.7: resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8" integrity sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg== +log4js@^6.9.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -2561,6 +2598,11 @@ resolve@^1.20.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -2673,6 +2715,15 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -2852,6 +2903,11 @@ typescript@4.8.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"