Skip to content

Commit 3a77caa

Browse files
[DECO-196] Wrap logger and cancellation token in a single context object in SDK. (#152)
Also - capture all error and non error requests made from api-client in the logs. - make request, response, error debug logs as 1 event. - Add logs to *&Wait APIs.
1 parent 578673d commit 3a77caa

File tree

24 files changed

+794
-454
lines changed

24 files changed

+794
-454
lines changed

packages/databricks-sdk-js/src/Redactor.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ export class Redactor {
1616
}
1717

1818
sanitize(
19-
obj: any,
19+
obj?: any,
2020
dropFields: string[] = [],
2121
maxFieldLength: number = 96
2222
): any {
23+
if (obj === undefined) {
24+
return undefined;
25+
}
26+
2327
if (isPrimitveType(obj)) {
2428
if (typeof obj === "string") {
2529
return onlyNBytes(obj, maxFieldLength);

packages/databricks-sdk-js/src/api-client.ts

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,15 @@ import * as https from "node:https";
33
import {TextDecoder} from "node:util";
44
import {fromDefaultChain} from "./auth/fromChain";
55
import {fetch} from "./fetch";
6-
import {NamedLogger, loggerInstance, logOpId, withLogContext} from "./logging";
7-
import {CancellationToken} from "./types";
6+
import {ExposedLoggers, Utils, withLogContext} from "./logging";
7+
import {context, Context} from "./context";
88

99
const sdkVersion = require("../package.json").version;
1010

1111
type HttpMethod = "POST" | "GET" | "DELETE" | "PATCH" | "PUT";
1212

1313
export class HttpError extends Error {
14-
constructor(
15-
readonly message: string,
16-
readonly code: number,
17-
readonly json?: any
18-
) {
14+
constructor(readonly message: string, readonly code: number) {
1915
super(message);
2016
}
2117
}
@@ -26,6 +22,21 @@ export class ApiClientResponseError extends Error {
2622
}
2723
}
2824

25+
function logAndReturnError(
26+
url: URL,
27+
request: any,
28+
response: any,
29+
error: unknown,
30+
context?: Context
31+
) {
32+
context?.logger?.error(url.toString(), {
33+
request,
34+
response,
35+
error: Utils.liftAllErrorProps(error),
36+
});
37+
return error;
38+
}
39+
2940
export class ApiClient {
3041
private agent: https.Agent;
3142
private _host?: URL;
@@ -63,14 +74,12 @@ export class ApiClient {
6374
return pairs.join(" ");
6475
}
6576

66-
@withLogContext("SDK")
77+
@withLogContext(ExposedLoggers.SDK)
6778
async request(
6879
path: string,
6980
method: HttpMethod,
7081
payload?: any,
71-
cancellationToken?: CancellationToken,
72-
@logOpId() opId?: string,
73-
@loggerInstance() logger?: NamedLogger
82+
@context context?: Context
7483
): Promise<Object> {
7584
const credentials = await this.credentialProvider();
7685
const headers = {
@@ -100,57 +109,71 @@ export class ApiClient {
100109
let response;
101110

102111
try {
103-
logger?.debug(url.toString(), {request: options});
104112
const {abort, response: responsePromise} = await fetch(
105113
url.toString(),
106114
options
107115
);
108-
if (cancellationToken?.onCancellationRequested) {
109-
cancellationToken?.onCancellationRequested(abort);
116+
if (context?.cancellationToken?.onCancellationRequested) {
117+
context?.cancellationToken?.onCancellationRequested(abort);
110118
}
111119
response = await responsePromise;
112120
} catch (e: any) {
113-
if (e.code && e.code === "ENOTFOUND") {
114-
throw new HttpError(`Can't connect to ${url.toString()}`, 500);
115-
} else {
116-
throw e;
117-
}
121+
const err =
122+
e.code && e.code === "ENOTFOUND"
123+
? new HttpError(`Can't connect to ${url.toString()}`, 500)
124+
: e;
125+
throw logAndReturnError(url, options, response, err, context);
118126
}
119127

120128
// throw error if the URL is incorrect and we get back an HTML page
121129
if (response.headers.get("content-type")?.match("text/html")) {
122-
throw new HttpError(`Can't connect to ${url.toString()}`, 404);
130+
throw logAndReturnError(
131+
url,
132+
options,
133+
response,
134+
new HttpError(`Can't connect to ${url.toString()}`, 404),
135+
context
136+
);
123137
}
124138

125139
let responseBody = await response.arrayBuffer();
126140
let responseText = new TextDecoder().decode(responseBody);
127141

128142
// TODO proper error handling
129143
if (!response.ok) {
130-
if (responseText.match(/invalid access token/i)) {
131-
throw new HttpError("Invalid access token", response.status);
132-
} else {
133-
throw new HttpError(responseText, response.status);
134-
}
144+
const err = responseText.match(/invalid access token/i)
145+
? new HttpError("Invalid access token", response.status)
146+
: new HttpError(responseText, response.status);
147+
throw logAndReturnError(url, options, responseText, err, context);
135148
}
136149

137150
try {
138151
response = JSON.parse(responseText);
139-
logger?.debug(url.toString(), {response: response});
140152
} catch (e) {
141-
throw new ApiClientResponseError(responseText, response);
153+
logAndReturnError(url, options, responseText, e, context);
154+
new ApiClientResponseError(responseText, response);
142155
}
143156

144157
if ("error" in response) {
158+
logAndReturnError(url, options, response, response.error, context);
145159
throw new ApiClientResponseError(response.error, response);
146160
}
147161

148162
if ("error_code" in response) {
149163
let message =
150164
response.message || `HTTP error ${response.error_code}`;
151-
throw new HttpError(message, response.error_code, response);
165+
throw logAndReturnError(
166+
url,
167+
options,
168+
response,
169+
new HttpError(message, response.error_code),
170+
context
171+
);
152172
}
153-
173+
context?.logger?.debug(url.toString(), {
174+
request: options,
175+
response: response,
176+
});
154177
return response as any;
155178
}
156179
}

packages/databricks-sdk-js/src/apis/.codegen/api.ts.tmpl

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import Time from "../../retries/Time";
88
import retry from "../../retries/retries";
99
import {CancellationToken} from "../../types"
1010
import {ApiError, ApiRetriableError} from "../apiError";
11+
import {context, Context} from "../../context"
12+
import {ExposedLoggers, withLogContext} from "../../logging";
1113

1214
{{range $i, $s := .Services }}
1315
export class {{$s.PascalName}}RetriableError extends ApiRetriableError {
@@ -31,15 +33,16 @@ export class {{$s.PascalName}}Service {
3133
/**
3234
{{.Comment " * " 80}}
3335
*/
36+
@withLogContext(ExposedLoggers.SDK)
3437
async {{.CamelName}}({{if .Request}}request: model.{{.Request.PascalName}},{{end}}
35-
cancellationToken?: CancellationToken
38+
@context context?: Context
3639
): Promise<model.{{if .Response}}{{.Response.PascalName}}{{else}}{{.EmptyResponseName.PascalName}}{{end}}> {
3740
const path = {{if .PathParts}}`{{range .PathParts}}{{.Prefix}}{{if .Field}}${request.{{.Field.SnakeName}}}{{end}}{{end}}`{{else}}"{{.Path}}"{{end}}
3841
return (await this.client.request(
3942
path,
4043
"{{.Verb}}",
4144
{{if .Request -}}request{{else}}undefined{{end}},
42-
cancellationToken
45+
context
4346
) as model.{{if .Response}}{{.Response.PascalName}}{{else}}{{.EmptyResponseName.PascalName}}{{end}})
4447
}
4548

@@ -48,28 +51,31 @@ export class {{$s.PascalName}}Service {
4851
* {{.CamelName}} and wait to reach {{range $i, $e := .Wait.Success}}{{if $i}} or {{end}}{{.Content}}{{end}} state
4952
* {{if .Wait.Failure}} or fail on reaching {{range $i, $e := .Wait.Failure}}{{if $i}} or {{end}}{{.Content}}{{end}} state{{end}}
5053
*/
54+
@withLogContext(ExposedLoggers.SDK)
5155
async {{.CamelName}}AndWait({{if .Request}}{{.Request.CamelName}}: model.{{.Request.PascalName}}{{end}},
5256
options?: {
5357
timeout?: Time,
54-
cancellationToken?: CancellationToken,
5558
onProgress?: (newPollResponse: model.{{if .Wait.Poll.Response}}{{.Wait.Poll.Response.PascalName}}{{else}}{{.Wait.Poll.EmptyResponseName.PascalName}}{{end}}) => Promise<void>
56-
}
59+
},
60+
@context context?: Context
5761
): Promise<model.{{if .Wait.Poll.Response}}{{.Wait.Poll.Response.PascalName}}{{else}}{{.Wait.Poll.EmptyResponseName.PascalName}}{{end}}> {
5862

5963
options = options || {};
6064
options.onProgress =
6165
options.onProgress || (async (newPollResponse) => {});
62-
let {cancellationToken, timeout, onProgress} = options;
66+
let {timeout, onProgress} = options;
67+
let cancellationToken = context?.cancellationToken;
6368

64-
{{if .Wait.ForceBindRequest}}{{else if .Response}}const {{.Response.CamelName}} = {{end}}await this.{{.CamelName}}({{.Request.CamelName}});
69+
{{if .Wait.ForceBindRequest}}{{else if .Response}}const {{.Response.CamelName}} = {{end}}await this.{{.CamelName}}({{if .Request}}{{.Request.CamelName}},{{end}} context);
6570

6671
return await retry<model.{{if .Wait.Poll.Response}}{{.Wait.Poll.Response.PascalName}}{{else}}{{.Wait.Poll.EmptyResponseName.PascalName}}{{end}}>({
6772
timeout,
6873
fn: async () => {
6974
const pollResponse = await this.{{.Wait.Poll.CamelName}}({ {{$e := .}}{{range .Wait.Binding}}
7075
{{.PollField.Name}}: {{.Bind.Of.CamelName}}.{{.Bind.Name}}!,{{end}}
71-
}, cancellationToken)
76+
}, context)
7277
if(cancellationToken?.isCancellationRequested) {
78+
context?.logger?.error("{{$s.PascalName}}.{{.CamelName}}AndWait: cancelled");
7379
throw new {{$s.PascalName}}Error("{{.CamelName}}AndWait", "cancelled");
7480
}
7581
await onProgress(pollResponse);
@@ -80,10 +86,14 @@ export class {{$s.PascalName}}Service {
8086
return pollResponse
8187
}
8288
{{if .Wait.Failure}}{{range $i, $e := .Wait.Failure}}{{if $i}} {{end}}case '{{$e.Content}}':{{end}}{
83-
throw new {{$s.PascalName}}Error("{{.CamelName}}AndWait", `failed to reach {{range $i, $e := .Wait.Success}}{{if $i}} or {{end}}{{.Content}}{{end}} state, got ${status}: ${statusMessage}`)
89+
const errorMessage = `failed to reach {{range $i, $e := .Wait.Success}}{{if $i}} or {{end}}{{.Content}}{{end}} state, got ${status}: ${statusMessage}`;
90+
context?.logger?.error(`{{$s.PascalName}}.{{.CamelName}}AndWait: ${errorMessage}`);
91+
throw new {{$s.PascalName}}Error("{{.CamelName}}AndWait", errorMessage);
8492
}
8593
{{end}}default:{
86-
throw new {{$s.PascalName}}RetriableError("{{.CamelName}}AndWait", `failed to reach {{range $i, $e := .Wait.Success}}{{if $i}} or {{end}}{{.Content}}{{end}} state, got ${status}: ${statusMessage}`)
94+
const errorMessage = `failed to reach {{range $i, $e := .Wait.Success}}{{if $i}} or {{end}}{{.Content}}{{end}} state, got ${status}: ${statusMessage}`;
95+
context?.logger?.error(`{{$s.PascalName}}.{{.CamelName}}AndWait: retrying: ${errorMessage}`);
96+
throw new {{$s.PascalName}}RetriableError("{{.CamelName}}AndWait", errorMessage);
8797
}
8898
}
8999
}

0 commit comments

Comments
 (0)