Skip to content

Commit 582bc0e

Browse files
authored
feat(hive-sdk): reduce http retry noise by introducing debug log level (#7264)
1 parent 4188530 commit 582bc0e

File tree

17 files changed

+211
-145
lines changed

17 files changed

+211
-145
lines changed

.changeset/odd-mammals-admire.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@graphql-hive/core': minor
3+
'@graphql-hive/apollo': minor
4+
'@graphql-hive/envelop': minor
5+
---
6+
7+
Introduce debug log level. HTTP retry log pollute the error log. The retries are now logged to the debug level.
8+
In order to see debug logs set the `debug` option to true.
9+
10+
```ts
11+
const hive = createHive({
12+
debug: true,
13+
})
14+
```
15+
16+
If you are using a custom logger, make sure to provide a `debug` logging method implementation.
17+
18+
```ts
19+
const hive = createHive({
20+
debug: true,
21+
agent: {
22+
logger: {
23+
info() {},
24+
error() {},
25+
debug() {}
26+
}
27+
}
28+
})
29+
```

.changeset/proud-jobs-say.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-hive/cli': patch
3+
---
4+
5+
Ensure http debug logs are printed properly.

packages/libraries/cli/src/base-command.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -214,19 +214,19 @@ export default abstract class BaseCommand<T extends typeof Command> extends Comm
214214
variables: args.variables,
215215
}),
216216
{
217-
logger: {
218-
info: (...args) => {
219-
if (isDebug) {
220-
this.logInfo(...args);
217+
logger: isDebug
218+
? {
219+
info: (...args) => {
220+
this.logInfo(...args);
221+
},
222+
error: (...args) => {
223+
this.logWarning(...args);
224+
},
225+
debug: (...args) => {
226+
this.logInfo(...args);
227+
},
221228
}
222-
},
223-
error: (...args) => {
224-
// Allow retrying requests without noise
225-
if (isDebug) {
226-
this.logWarning(...args);
227-
}
228-
},
229-
},
229+
: undefined,
230230
headers: requestHeaders,
231231
timeout: args.timeout,
232232
},

packages/libraries/core/src/client/agent.ts

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,10 @@ export function createAgent<TEvent>(
100100
headers?(): Record<string, string>;
101101
},
102102
) {
103-
const options: Required<Omit<AgentOptions, 'fetch' | 'circuitBreaker'>> & {
103+
const options: Required<Omit<AgentOptions, 'fetch' | 'circuitBreaker' | 'logger' | 'debug'>> & {
104104
circuitBreaker: null | AgentCircuitBreakerConfiguration;
105105
} = {
106106
timeout: 30_000,
107-
debug: false,
108107
enabled: true,
109108
minTimeout: 200,
110109
maxRetries: 3,
@@ -119,9 +118,10 @@ export function createAgent<TEvent>(
119118
: pluginOptions.circuitBreaker === false
120119
? null
121120
: pluginOptions.circuitBreaker,
122-
logger: createHiveLogger(pluginOptions.logger ?? console, '[agent]'),
123121
};
124122

123+
const logger = createHiveLogger(pluginOptions.logger ?? console, '[agent]');
124+
125125
const enabled = options.enabled !== false;
126126

127127
let timeoutID: ReturnType<typeof setTimeout> | null = null;
@@ -134,16 +134,6 @@ export function createAgent<TEvent>(
134134
timeoutID = setTimeout(send, options.sendInterval);
135135
}
136136

137-
function debugLog(msg: string) {
138-
if (options.debug) {
139-
options.logger.info(msg);
140-
}
141-
}
142-
143-
function errorLog(msg: string) {
144-
options.logger.error(msg);
145-
}
146-
147137
let scheduled = false;
148138
let inProgressCaptures: Promise<void>[] = [];
149139

@@ -173,14 +163,14 @@ export function createAgent<TEvent>(
173163
data.set(event);
174164

175165
if (data.size() >= options.maxSize) {
176-
debugLog('Sending immediately');
166+
logger.debug('Sending immediately');
177167
setImmediate(() => send({ throwOnError: false, skipSchedule: true }));
178168
}
179169
}
180170

181171
function sendImmediately(event: TEvent): Promise<ReadOnlyResponse | null> {
182172
data.set(event);
183-
debugLog('Sending immediately');
173+
logger.debug('Sending immediately');
184174
return send({ throwOnError: true, skipSchedule: true });
185175
}
186176

@@ -199,7 +189,7 @@ export function createAgent<TEvent>(
199189
retries: options.maxRetries,
200190
factor: 2,
201191
},
202-
logger: options.logger,
192+
logger,
203193
fetchImplementation: pluginOptions.fetch,
204194
signal,
205195
});
@@ -221,14 +211,14 @@ export function createAgent<TEvent>(
221211

222212
data.clear();
223213

224-
debugLog(`Sending report (queue ${dataToSend})`);
214+
logger.debug(`Sending report (queue ${dataToSend})`);
225215
const response = sendFromBreaker(buffer)
226216
.then(res => {
227-
debugLog(`Report sent!`);
217+
logger.debug(`Report sent!`);
228218
return res;
229219
})
230220
.catch(error => {
231-
errorLog(`Failed to send report.`);
221+
logger.debug(`Failed to send report.`);
232222

233223
if (sendOptions?.throwOnError) {
234224
throw error;
@@ -246,7 +236,7 @@ export function createAgent<TEvent>(
246236
}
247237

248238
async function dispose() {
249-
debugLog('Disposing');
239+
logger.debug('Disposing');
250240
if (timeoutID) {
251241
clearTimeout(timeoutID);
252242
}
@@ -266,7 +256,7 @@ export function createAgent<TEvent>(
266256
ReturnType<typeof sendHTTPCall>
267257
>;
268258
let loadCircuitBreakerPromise: Promise<void> | null = null;
269-
const breakerLogger = createHiveLogger(options.logger, '[circuit breaker]');
259+
const breakerLogger = createHiveLogger(logger, '[circuit breaker]');
270260

271261
function noopBreaker(): typeof breaker {
272262
return {

packages/libraries/core/src/client/client.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,32 @@ import { version } from '../version.js';
88
import { http } from './http-client.js';
99
import { createPersistedDocuments } from './persisted-documents.js';
1010
import { createReporting } from './reporting.js';
11-
import type { HiveClient, HivePluginOptions } from './types.js';
11+
import type { HiveClient, HiveInternalPluginOptions, HivePluginOptions } from './types.js';
1212
import { createUsage } from './usage.js';
13-
import { createHiveLogger, isLegacyAccessToken, logIf } from './utils.js';
13+
import { createHiveLogger, isLegacyAccessToken } from './utils.js';
1414

1515
export function createHive(options: HivePluginOptions): HiveClient {
16-
const logger = createHiveLogger(options?.agent?.logger ?? console, '[hive]');
16+
const logger = createHiveLogger(
17+
options?.agent?.logger ?? console,
18+
'[hive]',
19+
options.debug ?? false,
20+
);
1721
let enabled = options.enabled ?? true;
1822

19-
if (enabled === false) {
20-
logIf(
21-
options.debug === true &&
22-
// hive client can be used only for persisted documents, without the cdn or usage reporting.
23-
// hence, we dont want a misleading log message below saying that the plugin is disabled
24-
!options.experimental__persistedDocuments,
25-
'Plugin is not enabled.',
26-
logger.info,
27-
);
23+
if (enabled === false && !options.experimental__persistedDocuments) {
24+
logger.debug('Plugin is not enabled.');
2825
}
2926

3027
if (!options.token && enabled) {
3128
enabled = false;
3229
logger.info('Missing token, disabling.');
3330
}
3431

35-
const mergedOptions: HivePluginOptions = { ...options, enabled } as HivePluginOptions;
32+
const mergedOptions: HiveInternalPluginOptions = {
33+
...options,
34+
enabled,
35+
logger,
36+
} as HiveInternalPluginOptions;
3637

3738
const usage = createUsage(mergedOptions);
3839
const schemaReporter = createReporting(mergedOptions);

packages/libraries/core/src/client/gateways.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { version } from '../version.js';
22
import { http } from './http-client.js';
33
import type { SchemaFetcherOptions, ServicesFetcherOptions } from './types.js';
4-
import { createHash, joinUrl } from './utils.js';
4+
import { createHash, createHiveLogger, joinUrl } from './utils.js';
55

66
interface Schema {
77
sdl: string;
@@ -10,7 +10,7 @@ interface Schema {
1010
}
1111

1212
function createFetcher(options: SchemaFetcherOptions & ServicesFetcherOptions) {
13-
const logger = options.logger ?? console;
13+
const logger = createHiveLogger(options.logger ?? console, '');
1414
let cacheETag: string | null = null;
1515
let cached: {
1616
id: string;

packages/libraries/core/src/client/http-client.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import asyncRetry from 'async-retry';
22
import { abortSignalAny } from '@graphql-hive/signal';
33
import { crypto, fetch, URL } from '@whatwg-node/fetch';
4-
import type { Logger } from './types.js';
4+
import { Logger } from './types';
55

66
interface SharedConfig {
77
headers: Record<string, string>;
@@ -104,9 +104,10 @@ export async function makeFetchCall(
104104

105105
return await asyncRetry(
106106
async (bail, attempt) => {
107+
const isFinalAttempt = attempt > retries;
107108
const requestId = crypto.randomUUID();
108109

109-
logger?.info(
110+
logger?.debug?.(
110111
`${config.method} ${endpoint} (x-request-id=${requestId})` +
111112
(retries > 0 ? ' ' + getAttemptMessagePart(attempt, retries + 1) : ''),
112113
);
@@ -125,15 +126,25 @@ export async function makeFetchCall(
125126
},
126127
signal,
127128
}).catch((error: unknown) => {
128-
const logErrorMessage = () =>
129-
logger?.error(
129+
const logErrorMessage = () => {
130+
const msg =
130131
`${config.method} ${endpoint} (x-request-id=${requestId}) failed ${getDuration()}. ` +
131-
getErrorMessage(error),
132-
);
132+
getErrorMessage(error);
133+
134+
if (isFinalAttempt) {
135+
logger?.error(msg);
136+
return;
137+
}
138+
logger?.debug?.(msg);
139+
};
133140

134141
if (isAggregateError(error)) {
135142
for (const err of error.errors) {
136-
logger?.error(err);
143+
if (isFinalAttempt) {
144+
logger?.error(err);
145+
continue;
146+
}
147+
logger?.debug?.(String(err));
137148
}
138149

139150
logErrorMessage();
@@ -152,21 +163,24 @@ export async function makeFetchCall(
152163
}
153164

154165
if (isRequestOk(response)) {
155-
logger?.info(
166+
logger?.debug?.(
156167
`${config.method} ${endpoint} (x-request-id=${requestId}) succeeded with status ${response.status} ${getDuration()}.`,
157168
);
158169

159170
return response;
160171
}
161172

162-
logger?.error(
163-
`${config.method} ${endpoint} (x-request-id=${requestId}) failed with status ${response.status} ${getDuration()}: ${(await response.text()) || '<empty response body>'}`,
164-
);
165-
166-
if (retries > 0 && attempt > retries) {
173+
if (isFinalAttempt) {
174+
logger?.error(
175+
`${config.method} ${endpoint} (x-request-id=${requestId}) failed with status ${response.status} ${getDuration()}: ${(await response.text()) || '<empty response body>'}`,
176+
);
167177
logger?.error(
168178
`${config.method} ${endpoint} (x-request-id=${requestId}) retry limit exceeded after ${attempt} attempts.`,
169179
);
180+
} else {
181+
logger?.debug?.(
182+
`${config.method} ${endpoint} (x-request-id=${requestId}) failed with status ${response.status} ${getDuration()}: ${(await response.text()) || '<empty response body>'}`,
183+
);
170184
}
171185

172186
const error = new Error(

packages/libraries/core/src/client/persisted-documents.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import type { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue.js';
22
import LRU from 'tiny-lru';
33
import { http } from './http-client.js';
4-
import type { Logger, PersistedDocumentsConfiguration } from './types';
4+
import type { PersistedDocumentsConfiguration } from './types';
5+
import type { HiveLogger } from './utils.js';
56

67
type HeadersObject = {
78
get(name: string): string | null;
89
};
910

1011
export function createPersistedDocuments(
1112
config: PersistedDocumentsConfiguration & {
12-
logger: Logger;
13+
logger: HiveLogger;
1314
fetch?: typeof fetch;
1415
},
1516
): null | {

packages/libraries/core/src/client/reporting.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import { getDocumentNodeFromSchema } from '@graphql-tools/utils';
1111
import { version } from '../version.js';
1212
import type { SchemaPublishMutation } from './__generated__/types.js';
1313
import { http } from './http-client.js';
14-
import type { HivePluginOptions } from './types.js';
14+
import type { HiveInternalPluginOptions } from './types.js';
1515
import { createHiveLogger, logIf } from './utils.js';
1616

1717
export interface SchemaReporter {
1818
report(args: { schema: GraphQLSchema }): void;
1919
dispose(): Promise<void>;
2020
}
2121

22-
export function createReporting(pluginOptions: HivePluginOptions): SchemaReporter {
22+
export function createReporting(pluginOptions: HiveInternalPluginOptions): SchemaReporter {
2323
if (!pluginOptions.reporting || pluginOptions.enabled === false) {
2424
return {
2525
async report() {},
@@ -30,7 +30,7 @@ export function createReporting(pluginOptions: HivePluginOptions): SchemaReporte
3030
const token = pluginOptions.token;
3131
const selfHostingOptions = pluginOptions.selfHosting;
3232
const reportingOptions = pluginOptions.reporting;
33-
const logger = createHiveLogger(pluginOptions.agent?.logger ?? console, '[hive][reporting]');
33+
const logger = createHiveLogger(pluginOptions.logger, '[reporting]');
3434

3535
logIf(
3636
typeof reportingOptions.author !== 'string' || reportingOptions.author.length === 0,

packages/libraries/core/src/client/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue.js';
33
import type { AgentOptions } from './agent.js';
44
import type { autoDisposeSymbol, hiveClientSymbol } from './client.js';
55
import type { SchemaReporter } from './reporting.js';
6+
import { HiveLogger } from './utils.js';
67

78
type HeadersObject = {
89
get(name: string): string | null;
@@ -70,6 +71,7 @@ export interface ClientInfo {
7071
export interface Logger {
7172
info(msg: string): void;
7273
error(error: any, ...data: any[]): void;
74+
debug?(msg: string): void;
7375
}
7476

7577
export interface HiveUsagePluginOptions {
@@ -237,6 +239,7 @@ export type HivePluginOptions = OptionalWhenFalse<
237239
*
238240
* **Note:** The new access tokens do not support printing the token info. For every access token starting with `hvo1/`
239241
* no information will be printed.
242+
*
240243
* @deprecated This option will be removed in the future.
241244
*/
242245
printTokenInfo?: boolean;
@@ -257,6 +260,10 @@ export type HivePluginOptions = OptionalWhenFalse<
257260
'token'
258261
>;
259262

263+
export type HiveInternalPluginOptions = HivePluginOptions & {
264+
logger: HiveLogger;
265+
};
266+
260267
export type Maybe<T> = null | undefined | T;
261268

262269
export interface GraphQLErrorsResult {

0 commit comments

Comments
 (0)