Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: full HTTP request logging #5234

Merged
merged 16 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/chilly-balloons-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@graphql-hive/apollo': minor
---

Better HTTP info, error and debug logging.

For the supergraph manager, pass a `console` instance as the `logger` property.

```ts
import { createSupergraphManager } from '@graphql-hive/apollo';

const manager = createSupergraphManager({
...otherOptions,
logger: console,
})
```

For the supergraph SDL fetcher pass a `console` instance as the `logger` property.

```ts
import { createSupergraphSDLFetcher } from '@graphql-hive/apollo';

const manager = createSupergraphSDLFetcher({
...otherOptions,
logger: console,
})
```
7 changes: 7 additions & 0 deletions .changeset/fuzzy-readers-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphql-hive/core': minor
'@graphql-hive/yoga': minor
'@graphql-hive/apollo': minor
---

Improved logging output of HTTP requests and retires.
5 changes: 5 additions & 0 deletions .changeset/stupid-rabbits-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-hive/cli': minor
---

Provide debug logging for HTTP requests when setting environment variable `NODE_ENV` to `development`.
n1ru4l marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const OPERATIONS_PATHS = [
const rulesToExtends = Object.fromEntries(
Object.entries(guildConfig.rules).filter(([key]) =>
[
'no-implicit-coercion',
'import/first',
'no-restricted-globals',
'@typescript-eslint/no-unused-vars',
Expand Down Expand Up @@ -189,6 +188,7 @@ module.exports = {
'jsx-a11y/no-static-element-interactions': 'off',
'@next/next/no-html-link-for-pages': 'off',
'unicorn/no-negated-condition': 'off',
'no-implicit-coercion': 'off',
},
},
{
Expand Down
7 changes: 5 additions & 2 deletions packages/libraries/apollo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
http,
isHiveClient,
joinUrl,
Logger,
} from '@graphql-hive/core';
import { version } from './version.js';

Expand All @@ -17,6 +18,7 @@ export { atLeastOnceSampler, createSchemaFetcher, createServicesFetcher } from '
export interface SupergraphSDLFetcherOptions {
endpoint: string;
key: string;
logger?: Logger;
}

export function createSupergraphSDLFetcher(options: SupergraphSDLFetcherOptions) {
Expand Down Expand Up @@ -44,13 +46,13 @@ export function createSupergraphSDLFetcher(options: SupergraphSDLFetcherOptions)
return http
.get(endpoint, {
headers,
isRequestOk: response => response.status === 304 || response.ok,
retry: {
retryWhen: response => response.status >= 500,
okWhen: response => response.status === 304,
retries: 10,
maxTimeout: 200,
minTimeout: 1,
},
logger: options.logger,
})
.then(async response => {
if (response.ok) {
Expand Down Expand Up @@ -87,6 +89,7 @@ export function createSupergraphManager(
const fetchSupergraph = createSupergraphSDLFetcher({
endpoint: options.endpoint,
key: options.key,
logger: options.logger,
});
let timer: ReturnType<typeof setTimeout> | null = null;

Expand Down
8 changes: 4 additions & 4 deletions packages/libraries/apollo/tests/apollo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ test('should not interrupt the process', async () => {
logger,
},
reporting: {
endpoint: 'http://404.localhost/registry',
endpoint: 'http://404.localhost.noop/registry',
author: 'jest',
commit: 'js',
},
usage: {
endpoint: 'http://404.localhost/usage',
endpoint: 'http://404.localhost.noop/usage',
},
}),
],
Expand All @@ -100,7 +100,7 @@ test('should not interrupt the process', async () => {
}
`,
});
await waitFor(50);
await waitFor(200);
await apollo.stop();
clean();
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('[hive][info]'));
Expand Down Expand Up @@ -299,7 +299,7 @@ describe('supergraph SDL fetcher', async () => {
await fetcher();
} catch (err) {
expect(err).toMatchInlineSnapshot(
`[Error: Failed to fetch http://localhost/supergraph, received: 500 Internal Server Error]`,
`[Error: GET http://localhost/supergraph failed with status 500.]`,
);
}
});
Expand Down
11 changes: 11 additions & 0 deletions packages/libraries/cli/src/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,17 @@ export default abstract class extends Command {
variables,
}),
{
logger: {
info: (...args) => {
// eslint-disable-next-line no-process-env
if (process.env.NODE_ENV === 'development') {
console.info(...args);
}
},
error: (...args) => {
console.error(...args);
},
},
headers: requestHeaders,
},
);
Expand Down
3 changes: 0 additions & 3 deletions packages/libraries/cli/src/commands/artifact/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ export default class ArtifactsFetch extends Command {
},
retry: {
retries: 3,
retryWhen(response) {
return response.status >= 500;
},
},
});

Expand Down
144 changes: 44 additions & 100 deletions packages/libraries/core/src/client/agent.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import retry from 'async-retry';
import { version } from '../version.js';
import { http } from './http-client.js';
import type { Logger } from './types.js';

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

export interface AgentOptions {
enabled?: boolean;
Expand Down Expand Up @@ -55,12 +54,10 @@ export interface AgentOptions {
export function createAgent<TEvent>(
pluginOptions: AgentOptions,
{
prefix,
data,
body,
headers = () => ({}),
}: {
prefix: string;
data: {
clear(): void;
set(data: TEvent): void;
Expand Down Expand Up @@ -97,10 +94,14 @@ export function createAgent<TEvent>(

function debugLog(msg: string) {
if (options.debug) {
options.logger.info(`[hive][${prefix}]${enabled ? '' : '[DISABLED]'} ${msg}`);
options.logger.info(msg);
}
}

function errorLog(msg: string) {
options.logger.error(msg);
}

let scheduled = false;
let inProgressCaptures: Promise<void>[] = [];

Expand Down Expand Up @@ -132,115 +133,59 @@ export function createAgent<TEvent>(

if (data.size() >= options.maxSize) {
debugLog('Sending immediately');
setImmediate(() => send({ runOnce: true, throwOnError: false }));
setImmediate(() => send({ throwOnError: false }));
}
}

function sendImmediately(event: TEvent): Promise<ReadOnlyResponse | null> {
data.set(event);

debugLog('Sending immediately');
return send({ runOnce: true, throwOnError: true });
return send({ throwOnError: true });
}

async function send(sendOptions: {
runOnce?: boolean;
throwOnError: true;
}): Promise<ReadOnlyResponse | null>;
async function send(sendOptions: {
runOnce?: boolean;
throwOnError: false;
}): Promise<ReadOnlyResponse | null>;
async function send(sendOptions?: {
runOnce?: boolean;
throwOnError: boolean;
}): Promise<ReadOnlyResponse | null> {
const runOnce = sendOptions?.runOnce ?? false;

if (!data.size()) {
if (!runOnce) {
schedule();
}
async function send(sendOptions?: { throwOnError?: boolean }): Promise<ReadOnlyResponse | null> {
if (!data.size() || !enabled) {
return null;
}

try {
const buffer = await body();
const dataToSend = data.size();

data.clear();

const sendReport: retry.RetryFunction<{
status: number;
text(): Promise<string>;
json(): Promise<unknown>;
}> = async (_bail, attempt) => {
debugLog(`Sending (queue ${dataToSend}) (attempt ${attempt})`);

if (!enabled) {
return {
status: 200,
text: async () => 'OK',
json: async () => ({}),
};
}

const response = await http
.post(options.endpoint, buffer, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
Authorization: `Bearer ${options.token}`,
'User-Agent': `${options.name}/${version}`,
...headers(),
},
timeout: options.timeout,
fetchImplementation: pluginOptions.__testing?.fetch,
})
.catch(error => {
debugLog(`Attempt ${attempt} failed: ${error.message}`);
return Promise.reject(error);
});

if (response.status >= 200 && response.status < 300) {
return response;
const buffer = await body();
const dataToSend = data.size();

data.clear();

debugLog(`Sending report (queue ${dataToSend})`);
const response = await http
.post(options.endpoint, buffer, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
Authorization: `Bearer ${options.token}`,
'User-Agent': `${options.name}/${version}`,
...headers(),
},
timeout: options.timeout,
retry: {
retries: options.maxRetries,
factor: 2,
},
logger: options.logger,
fetchImplementation: pluginOptions.__testing?.fetch,
})
.then(res => {
debugLog(`Report sent!`);
return res;
})
.catch(error => {
errorLog(`Failed to send report.`);

if (sendOptions?.throwOnError) {
throw error;
}

debugLog(`Attempt ${attempt} failed: ${response.status}`);
throw new Error(`${response.status}: ${response.statusText} ${await response.text()}`);
};

const response = await retry(sendReport, {
retries: options.maxRetries,
minTimeout: options.minTimeout,
factor: 2,
return null;
});

if (response.status < 200 || response.status >= 300) {
throw new Error(
`[hive][${prefix}] Failed to send data (HTTP status ${response.status}): ${await response.text()}`,
);
}

debugLog(`Sent!`);

if (!runOnce) {
schedule();
}
return response;
} catch (error: any) {
if (!runOnce) {
schedule();
}

if (sendOptions?.throwOnError) {
throw error;
}

options.logger.error(`[hive][${prefix}] Failed to send data: ${error.message}`);

return null;
}
return response;
}

async function dispose() {
Expand All @@ -254,7 +199,6 @@ export function createAgent<TEvent>(
}

await send({
runOnce: true,
throwOnError: false,
});
}
Expand Down
Loading
Loading