Skip to content

Commit 4c1cc84

Browse files
n1ru4lenisdenjo
andcommitted
feat: hive-logger for JS SDK (#7307)
Co-authored-by: Denis Badurina <[email protected]>
1 parent 876c5a5 commit 4c1cc84

File tree

22 files changed

+368
-299
lines changed

22 files changed

+368
-299
lines changed

packages/libraries/apollo/tests/apollo.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ test('should not interrupt the process', async () => {
102102
await waitFor(200);
103103
await apollo.stop();
104104
clean();
105-
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('[hive][info]'));
106-
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('[hive][usage]'));
107-
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('[hive][reporting]'));
105+
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('[hive][info]'));
106+
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('[hive][usage]'));
107+
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('[hive][reporting]'));
108108
}, 1_000);
109109

110110
test('should capture client name and version headers', async () => {

packages/libraries/cli/src/commands/artifact/fetch.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,18 +69,19 @@ export default class ArtifactsFetch extends Command<typeof ArtifactsFetch> {
6969
retry: {
7070
retries: 3,
7171
},
72-
logger: {
73-
info: (...args) => {
74-
if (this.flags.debug) {
75-
console.info(...args);
72+
logger: this.flags.debug
73+
? {
74+
info: (...args: Array<unknown>) => {
75+
this.logInfo(...args);
76+
},
77+
error: (...args: Array<unknown>) => {
78+
this.logFailure(...args);
79+
},
80+
debug: (...args: Array<unknown>) => {
81+
this.logInfo(...args);
82+
},
7683
}
77-
},
78-
error: (...args) => {
79-
if (this.flags.debug) {
80-
console.error(...args);
81-
}
82-
},
83-
},
84+
: undefined,
8485
});
8586
} catch (e: any) {
8687
const sourceError = e?.cause ?? e;

packages/libraries/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"graphql": "^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
4747
},
4848
"dependencies": {
49+
"@graphql-hive/logger": "^1.0.9",
4950
"@graphql-hive/signal": "^2.0.0",
5051
"@graphql-tools/utils": "^10.0.0",
5152
"@whatwg-node/fetch": "^0.10.13",

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import CircuitBreaker from '../circuit-breaker/circuit.js';
22
import { version } from '../version.js';
33
import { http } from './http-client.js';
4-
import type { Logger } from './types.js';
5-
import { createHiveLogger } from './utils.js';
4+
import type { LegacyLogger } from './types.js';
5+
import { chooseLogger } from './utils.js';
66

77
type ReadOnlyResponse = Pick<Response, 'status' | 'text' | 'json' | 'statusText'>;
88

@@ -73,7 +73,7 @@ export interface AgentOptions {
7373
*
7474
* @deprecated Instead, provide a logger for the root Hive SDK. If a logger is provided on the root Hive SDK, this one is ignored.
7575
*/
76-
logger?: Logger;
76+
logger?: LegacyLogger;
7777
/**
7878
* Circuit Breaker Configuration.
7979
* true -> Use default configuration
@@ -123,13 +123,13 @@ export function createAgent<TEvent>(
123123
? null
124124
: pluginOptions.circuitBreaker,
125125
};
126-
const logger = createHiveLogger(pluginOptions.logger ?? console, '[agent]');
126+
const logger = chooseLogger(pluginOptions.logger).child('[agent]');
127127

128128
let circuitBreaker: CircuitBreakerInterface<
129129
Parameters<typeof sendHTTPCall>,
130130
ReturnType<typeof sendHTTPCall>
131131
>;
132-
const breakerLogger = createHiveLogger(logger, '[circuit breaker]');
132+
const breakerLogger = logger.child('[circuit breaker]');
133133

134134
const enabled = options.enabled !== false;
135135
let timeoutID: ReturnType<typeof setTimeout> | null = null;

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

Lines changed: 14 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,67 +4,31 @@ import {
44
type GraphQLSchema,
55
type subscribe as SubscribeImplementation,
66
} from 'graphql';
7+
import { Logger } from '@graphql-hive/logger';
78
import { version } from '../version.js';
89
import { http } from './http-client.js';
910
import { createPersistedDocuments } from './persisted-documents.js';
1011
import { createReporting } from './reporting.js';
1112
import type { HiveClient, HiveInternalPluginOptions, HivePluginOptions } from './types.js';
1213
import { createUsage } from './usage.js';
13-
import { createHiveLogger, HiveLogger, isLegacyAccessToken } from './utils.js';
14-
15-
function chooseDefaultLogger(options: HivePluginOptions): HiveLogger {
16-
if (options.logger === 'debug') {
17-
return createHiveLogger(
18-
{
19-
debug(...args) {
20-
console.debug(...args);
21-
},
22-
info(...args) {
23-
console.info(...args);
24-
},
25-
error(...args) {
26-
console.error(...args);
27-
},
28-
},
29-
'[hive]',
30-
);
31-
}
32-
if (options.logger === 'info') {
33-
return createHiveLogger(
34-
{
35-
debug() {},
36-
info(...args) {
37-
console.info(...args);
38-
},
39-
error(...args) {
40-
console.error(...args);
41-
},
42-
},
43-
'[hive]',
44-
);
14+
import { chooseLogger, isLegacyAccessToken } from './utils.js';
15+
16+
function resolveLoggerFromConfigOptions(options: HivePluginOptions): Logger {
17+
if (typeof options.logger == 'string') {
18+
return new Logger({
19+
level: options.logger,
20+
});
4521
}
46-
if (options.logger === 'error') {
47-
return createHiveLogger(
48-
{
49-
debug() {},
50-
info() {},
51-
error(...args) {
52-
console.error(...args);
53-
},
54-
},
55-
'[hive]',
56-
);
22+
23+
if (options.logger instanceof Logger) {
24+
return options.logger;
5725
}
5826

59-
return createHiveLogger(
60-
options?.logger ?? options?.agent?.logger ?? console,
61-
'[hive]',
62-
options.debug,
63-
);
27+
return chooseLogger(options.logger ?? options.agent?.logger, options.debug);
6428
}
6529

6630
export function createHive(options: HivePluginOptions): HiveClient {
67-
const logger = chooseDefaultLogger(options);
31+
const logger = resolveLoggerFromConfigOptions(options).child('[hive]');
6832
let enabled = options.enabled ?? true;
6933

7034
if (enabled === false && !options.experimental__persistedDocuments) {
@@ -111,7 +75,7 @@ export function createHive(options: HivePluginOptions): HiveClient {
11175
? options.printTokenInfo === true || (!!options.debug && options.printTokenInfo !== false)
11276
: false;
11377

114-
const infoLogger = createHiveLogger(logger, '[info]');
78+
const infoLogger = logger.child('[info]');
11579

11680
const info = printTokenInfo
11781
? async () => {

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, createHiveLogger, joinUrl } from './utils.js';
4+
import { chooseLogger, createHash, 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 = createHiveLogger(options.logger ?? console, '');
13+
const logger = chooseLogger(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: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import asyncRetry from 'async-retry';
2+
import { Logger } from '@graphql-hive/logger';
23
import { abortSignalAny } from '@graphql-hive/signal';
34
import { crypto, fetch, URL } from '@whatwg-node/fetch';
4-
import { Logger } from './types';
5+
import type { LegacyLogger } from './types';
56

67
interface SharedConfig {
78
headers: Record<string, string>;
@@ -15,7 +16,7 @@ interface SharedConfig {
1516
/** custom fetch implementation. */
1617
fetchImplementation?: typeof fetch;
1718
/** Logger for HTTP info and request errors. Uses `console` by default. */
18-
logger?: Logger;
19+
logger?: LegacyLogger;
1920
/**
2021
* Function for determining whether the request response is okay.
2122
* You can override it if you want to accept other status codes as well.
@@ -58,6 +59,39 @@ export const http = {
5859
post,
5960
};
6061

62+
function chooseLogger(logger: SharedConfig['logger']): Logger {
63+
if (!logger) {
64+
return new Logger({
65+
writers: [{ write() {} }],
66+
});
67+
}
68+
69+
if (logger instanceof Logger) {
70+
return logger;
71+
}
72+
73+
return new Logger({
74+
level: 'debug',
75+
writers: [
76+
{
77+
write(level, _attrs, msg) {
78+
if (level === 'debug' && logger.debug && msg) {
79+
logger.debug(msg);
80+
return;
81+
}
82+
if (level === 'info' && msg) {
83+
logger.info(msg);
84+
return;
85+
}
86+
if (level === 'error' && msg) {
87+
logger.error(msg);
88+
}
89+
},
90+
},
91+
],
92+
});
93+
}
94+
6195
export async function makeFetchCall(
6296
endpoint: URL | string,
6397
config: {
@@ -74,7 +108,7 @@ export async function makeFetchCall(
74108
/** custom fetch implementation. */
75109
fetchImplementation?: typeof fetch;
76110
/** Logger for HTTP info and request errors. Uses `console` by default. */
77-
logger?: Logger;
111+
logger?: LegacyLogger;
78112
/**
79113
* Function for determining whether the request response is okay.
80114
* You can override it if you want to accept other status codes as well.
@@ -85,7 +119,7 @@ export async function makeFetchCall(
85119
signal?: AbortSignal;
86120
},
87121
): Promise<Response> {
88-
const logger = config.logger;
122+
const logger = chooseLogger(config.logger);
89123
const isRequestOk: ResponseAssertFunction = config.isRequestOk ?? (response => response.ok);
90124
let retries = 0;
91125
let minTimeout = 200;
@@ -107,7 +141,7 @@ export async function makeFetchCall(
107141
const isFinalAttempt = attempt > retries;
108142
const requestId = crypto.randomUUID();
109143

110-
logger?.debug?.(
144+
logger.debug(
111145
`${config.method} ${endpoint} (x-request-id=${requestId})` +
112146
(retries > 0 ? ' ' + getAttemptMessagePart(attempt, retries + 1) : ''),
113147
);
@@ -126,33 +160,16 @@ export async function makeFetchCall(
126160
},
127161
signal,
128162
}).catch((error: unknown) => {
129-
const logErrorMessage = () => {
130-
const msg =
131-
`${config.method} ${endpoint} (x-request-id=${requestId}) failed ${getDuration()}. ` +
132-
getErrorMessage(error);
133-
134-
if (isFinalAttempt) {
135-
logger?.error(msg);
136-
return;
137-
}
138-
logger?.debug?.(msg);
139-
};
140-
141-
if (isAggregateError(error)) {
142-
for (const err of error.errors) {
143-
if (isFinalAttempt) {
144-
logger?.error(err);
145-
continue;
146-
}
147-
logger?.debug?.(String(err));
148-
}
149-
150-
logErrorMessage();
151-
throw new Error(`Unexpected HTTP error. (x-request-id=${requestId})`, { cause: error });
163+
const msg =
164+
`${config.method} ${endpoint} (x-request-id=${requestId}) failed ${getDuration()}. ` +
165+
getErrorMessage(error);
166+
167+
if (isFinalAttempt) {
168+
logger.error({ error }, msg);
169+
} else {
170+
logger.debug({ error }, msg);
152171
}
153172

154-
logger?.error(error);
155-
logErrorMessage();
156173
throw new Error(`Unexpected HTTP error. (x-request-id=${requestId})`, { cause: error });
157174
});
158175

@@ -171,14 +188,14 @@ export async function makeFetchCall(
171188
}
172189

173190
if (isFinalAttempt) {
174-
logger?.error(
191+
logger.error(
175192
`${config.method} ${endpoint} (x-request-id=${requestId}) failed with status ${response.status} ${getDuration()}: ${(await response.text()) || '<empty response body>'}`,
176193
);
177-
logger?.error(
194+
logger.error(
178195
`${config.method} ${endpoint} (x-request-id=${requestId}) retry limit exceeded after ${attempt} attempts.`,
179196
);
180197
} else {
181-
logger?.debug?.(
198+
logger.debug(
182199
`${config.method} ${endpoint} (x-request-id=${requestId}) failed with status ${response.status} ${getDuration()}: ${(await response.text()) || '<empty response body>'}`,
183200
);
184201
}
@@ -189,7 +206,7 @@ export async function makeFetchCall(
189206

190207
if (response.status >= 400 && response.status < 500) {
191208
if (retries > 0) {
192-
logger?.error(`Abort retry because of status code ${response.status}.`);
209+
logger.error(`Abort retry because of status code ${response.status}.`);
193210
}
194211
bail(error);
195212
}
@@ -247,12 +264,4 @@ function formatTimestamp(timestamp: number): string {
247264
return parts.join(':');
248265
}
249266

250-
interface AggregateError extends Error {
251-
errors: Error[];
252-
}
253-
254-
function isAggregateError(error: unknown): error is AggregateError {
255-
return !!error && typeof error === 'object' && 'errors' in error && Array.isArray(error.errors);
256-
}
257-
258267
export { URL };

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

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

77
type HeadersObject = {
88
get(name: string): string | null;
99
};
1010

1111
export function createPersistedDocuments(
1212
config: PersistedDocumentsConfiguration & {
13-
logger: HiveLogger;
13+
logger: Logger;
1414
fetch?: typeof fetch;
1515
},
1616
): null | {

0 commit comments

Comments
 (0)