Skip to content

Commit 44ea0ad

Browse files
stainless-botRobertCraigie
authored andcommitted
feat(client): improve debug logs
chore: unknown commit message
1 parent 5ca2478 commit 44ea0ad

File tree

7 files changed

+208
-22
lines changed

7 files changed

+208
-22
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,7 @@ const client = new OpenAI({
596596
});
597597
```
598598

599-
Note that if given a `DEBUG=true` environment variable, this library will log all requests and responses automatically.
599+
Note that if given a `OPENAI_LOG=debug` environment variable, this library will log all requests and responses automatically.
600600
This is intended for debugging purposes only and may change in the future without notice.
601601

602602
### Configuring an HTTP(S) Agent (e.g., for proxies)

src/api-promise.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

3+
import { type OpenAI } from './client';
4+
35
import { type PromiseOrValue } from './internal/types';
46
import {
57
type APIResponseProps,
@@ -14,10 +16,13 @@ import {
1416
*/
1517
export class APIPromise<T> extends Promise<WithRequestID<T>> {
1618
private parsedPromise: Promise<WithRequestID<T>> | undefined;
19+
#client: OpenAI;
1720

1821
constructor(
22+
client: OpenAI,
1923
private responsePromise: Promise<APIResponseProps>,
2024
private parseResponse: (
25+
client: OpenAI,
2126
props: APIResponseProps,
2227
) => PromiseOrValue<WithRequestID<T>> = defaultParseResponse,
2328
) {
@@ -27,11 +32,12 @@ export class APIPromise<T> extends Promise<WithRequestID<T>> {
2732
// to parse the response
2833
resolve(null as any);
2934
});
35+
this.#client = client;
3036
}
3137

3238
_thenUnwrap<U>(transform: (data: T, props: APIResponseProps) => U): APIPromise<U> {
33-
return new APIPromise(this.responsePromise, async (props) =>
34-
addRequestID(transform(await this.parseResponse(props), props), props.response),
39+
return new APIPromise(this.#client, this.responsePromise, async (client, props) =>
40+
addRequestID(transform(await this.parseResponse(client, props), props), props.response),
3541
);
3642
}
3743

@@ -69,7 +75,9 @@ export class APIPromise<T> extends Promise<WithRequestID<T>> {
6975

7076
private parse(): Promise<WithRequestID<T>> {
7177
if (!this.parsedPromise) {
72-
this.parsedPromise = this.responsePromise.then(this.parseResponse) as any as Promise<WithRequestID<T>>;
78+
this.parsedPromise = this.responsePromise.then((data) =>
79+
this.parseResponse(this.#client, data),
80+
) as any as Promise<WithRequestID<T>>;
7381
}
7482
return this.parsedPromise;
7583
}

src/client.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import type { RequestInit, RequestInfo, BodyInit } from './internal/builtin-types';
44
import type { HTTPMethod, PromiseOrValue } from './internal/types';
5-
import { debug } from './internal/utils/log';
65
import { uuid4 } from './internal/utils/uuid';
76
import { validatePositiveInteger, isAbsoluteURL } from './internal/utils/values';
87
import { sleep } from './internal/utils/sleep';
@@ -81,6 +80,7 @@ import {
8180
Moderations,
8281
} from './resources/moderations';
8382
import { readEnv } from './internal/utils/env';
83+
import { logger } from './internal/utils/log';
8484
import { isEmptyObj } from './internal/utils/values';
8585
import { Audio, AudioModel, AudioResponseFormat } from './resources/audio/audio';
8686
import { Beta } from './resources/beta/beta';
@@ -134,6 +134,25 @@ const safeJSON = (text: string) => {
134134
}
135135
};
136136

137+
type LogFn = (message: string, ...rest: unknown[]) => void;
138+
export type Logger = {
139+
error: LogFn;
140+
warn: LogFn;
141+
info: LogFn;
142+
debug: LogFn;
143+
};
144+
export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug';
145+
const isLogLevel = (key: string | undefined): key is LogLevel => {
146+
const levels: Record<LogLevel, true> = {
147+
off: true,
148+
error: true,
149+
warn: true,
150+
info: true,
151+
debug: true,
152+
};
153+
return key! in levels;
154+
};
155+
137156
export interface ClientOptions {
138157
/**
139158
* Defaults to process.env['OPENAI_API_KEY'].
@@ -210,6 +229,20 @@ export interface ClientOptions {
210229
* Only set this option to `true` if you understand the risks and have appropriate mitigations in place.
211230
*/
212231
dangerouslyAllowBrowser?: boolean;
232+
233+
/**
234+
* Set the log level.
235+
*
236+
* Defaults to process.env['OPENAI_LOG'].
237+
*/
238+
logLevel?: LogLevel | undefined | null;
239+
240+
/**
241+
* Set the logger.
242+
*
243+
* Defaults to globalThis.console.
244+
*/
245+
logger?: Logger | undefined | null;
213246
}
214247

215248
type FinalizedRequestInit = RequestInit & { headers: Headers };
@@ -225,6 +258,8 @@ export class OpenAI {
225258
baseURL: string;
226259
maxRetries: number;
227260
timeout: number;
261+
logger: Logger | undefined;
262+
logLevel: LogLevel | undefined;
228263
httpAgent: Shims.Agent | undefined;
229264

230265
private fetch: Fetch;
@@ -276,6 +311,15 @@ export class OpenAI {
276311

277312
this.baseURL = options.baseURL!;
278313
this.timeout = options.timeout ?? OpenAI.DEFAULT_TIMEOUT /* 10 minutes */;
314+
this.logger = options.logger ?? console;
315+
if (options.logLevel != null) {
316+
this.logLevel = options.logLevel;
317+
} else {
318+
const envLevel = readEnv('OPENAI_LOG');
319+
if (isLogLevel(envLevel)) {
320+
this.logLevel = envLevel;
321+
}
322+
}
279323
this.httpAgent = options.httpAgent;
280324
this.maxRetries = options.maxRetries ?? 2;
281325
this.fetch = options.fetch ?? Shims.getDefaultFetch();
@@ -415,7 +459,7 @@ export class OpenAI {
415459
options: PromiseOrValue<FinalRequestOptions>,
416460
remainingRetries: number | null = null,
417461
): APIPromise<Rsp> {
418-
return new APIPromise(this.makeRequest(options, remainingRetries));
462+
return new APIPromise(this, this.makeRequest(options, remainingRetries));
419463
}
420464

421465
private async makeRequest(
@@ -434,7 +478,7 @@ export class OpenAI {
434478

435479
await this.prepareRequest(req, { url, options });
436480

437-
debug('request', url, options, req.headers);
481+
logger(this).debug('request', url, options, req.headers);
438482

439483
if (options.signal?.aborted) {
440484
throw new Errors.APIUserAbortError();
@@ -459,7 +503,7 @@ export class OpenAI {
459503
if (!response.ok) {
460504
if (retriesRemaining && this.shouldRetry(response)) {
461505
const retryMessage = `retrying, ${retriesRemaining} attempts remaining`;
462-
debug(`response (error; ${retryMessage})`, response.status, url, response.headers);
506+
logger(this).debug(`response (error; ${retryMessage})`, response.status, url, response.headers);
463507
return this.retryRequest(options, retriesRemaining, response.headers);
464508
}
465509

@@ -468,7 +512,13 @@ export class OpenAI {
468512
const errMessage = errJSON ? undefined : errText;
469513
const retryMessage = retriesRemaining ? `(error; no more retries left)` : `(error; not retryable)`;
470514

471-
debug(`response (error; ${retryMessage})`, response.status, url, response.headers, errMessage);
515+
logger(this).debug(
516+
`response (error; ${retryMessage})`,
517+
response.status,
518+
url,
519+
response.headers,
520+
errMessage,
521+
);
472522

473523
const err = this.makeStatusError(response.status, errJSON, errMessage, response.headers);
474524
throw err;

src/internal/parse.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

3-
import { debug } from './utils/log';
4-
import { FinalRequestOptions } from './request-options';
3+
import type { FinalRequestOptions } from './request-options';
54
import { Stream } from '../streaming';
5+
import { type OpenAI } from '../client';
6+
import { logger } from './utils/log';
67
import type { AbstractPage } from '../pagination';
78

89
export type APIResponseProps = {
@@ -11,10 +12,13 @@ export type APIResponseProps = {
1112
controller: AbortController;
1213
};
1314

14-
export async function defaultParseResponse<T>(props: APIResponseProps): Promise<WithRequestID<T>> {
15+
export async function defaultParseResponse<T>(
16+
client: OpenAI,
17+
props: APIResponseProps,
18+
): Promise<WithRequestID<T>> {
1519
const { response } = props;
1620
if (props.options.stream) {
17-
debug('response', response.status, response.url, response.headers, response.body);
21+
logger(client).debug('response', response.status, response.url, response.headers, response.body);
1822

1923
// Note: there is an invariant here that isn't represented in the type system
2024
// that if you set `stream: true` the response type must also be `Stream<T>`
@@ -41,13 +45,13 @@ export async function defaultParseResponse<T>(props: APIResponseProps): Promise<
4145
if (isJSON) {
4246
const json = await response.json();
4347

44-
debug('response', response.status, response.url, response.headers, json);
48+
logger(client).debug('response', response.status, response.url, response.headers, json);
4549

4650
return addRequestID(json as T, response);
4751
}
4852

4953
const text = await response.text();
50-
debug('response', response.status, response.url, response.headers, text);
54+
logger(client).debug('response', response.status, response.url, response.headers, text);
5155

5256
// TODO handle blob, arraybuffer, other content types, etc.
5357
return text as unknown as WithRequestID<T>;

src/internal/utils/log.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,49 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

3-
import { readEnv } from './env';
3+
import type { LogLevel, Logger } from '../../client';
4+
import { type OpenAI } from '../../client';
45

5-
export function debug(action: string, ...args: any[]) {
6-
if (readEnv('DEBUG') === 'true') {
7-
console.log(`OpenAI:DEBUG:${action}`, ...args);
6+
const levelNumbers = {
7+
off: 0,
8+
error: 200,
9+
warn: 300,
10+
info: 400,
11+
debug: 500,
12+
};
13+
14+
function noop() {}
15+
16+
function logFn(logger: Logger | undefined, clientLevel: LogLevel | undefined, level: keyof Logger) {
17+
if (!logger || levelNumbers[level] > levelNumbers[clientLevel!]!) {
18+
return noop;
19+
} else {
20+
// Don't wrap logger functions, we want the stacktrace intact!
21+
return logger[level].bind(logger);
22+
}
23+
}
24+
25+
let lastLogger: { deref(): Logger } | undefined;
26+
let lastLevel: LogLevel | undefined;
27+
let lastLevelLogger: Logger;
28+
29+
export function logger(client: OpenAI): Logger {
30+
let { logger, logLevel: clientLevel } = client;
31+
if (lastLevel === clientLevel && (logger === lastLogger || logger === lastLogger?.deref())) {
32+
return lastLevelLogger;
833
}
34+
const levelLogger = {
35+
error: logFn(logger, clientLevel, 'error'),
36+
warn: logFn(logger, clientLevel, 'warn'),
37+
info: logFn(logger, clientLevel, 'info'),
38+
debug: logFn(logger, clientLevel, 'debug'),
39+
};
40+
const { WeakRef } = globalThis as any;
41+
lastLogger =
42+
logger ?
43+
WeakRef ? new WeakRef(logger)
44+
: { deref: () => logger }
45+
: undefined;
46+
lastLevel = clientLevel;
47+
lastLevelLogger = levelLogger;
48+
return levelLogger;
949
}

src/pagination.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

3-
import type { OpenAI } from './client';
43
import { OpenAIError } from './error';
54
import { FinalRequestOptions } from './internal/request-options';
65
import { defaultParseResponse, WithRequestID } from './internal/parse';
76
import { APIPromise } from './api-promise';
7+
import { type OpenAI } from './client';
88
import { type APIResponseProps } from './internal/parse';
99
import { maybeObj } from './internal/utils/values';
1010

@@ -86,12 +86,13 @@ export class PagePromise<
8686
Page: new (...args: ConstructorParameters<typeof AbstractPage>) => PageClass,
8787
) {
8888
super(
89+
client,
8990
request,
90-
async (props) =>
91+
async (client, props) =>
9192
new Page(
9293
client,
9394
props.response,
94-
await defaultParseResponse(props),
95+
await defaultParseResponse(client, props),
9596
props.options,
9697
) as WithRequestID<PageClass>,
9798
);

0 commit comments

Comments
 (0)