Skip to content

Commit e6eeba1

Browse files
[DECO-102] Setup logging infra (#117)
Example request response logs ```json [ { "level": "debug", "message": "REDACTED/api/2.0/preview/scim/v2/Me", "operationId": "b376e4c8-9140-49f5-873f-53dc4311c626", "operationName": "ApiClient.request", "request": { "agent": { "_events": {}, "_eventsCount": 2, "_sessionCache": { "list": [], "map": {} }, "defaultPort": 443, "freeSockets": {}, "keepAlive": true, "keepAliveMsecs": 15000, "maxCachedSessions": 100, "maxFreeSockets": 256, "maxSockets": null, "maxTotalSockets": null, "options": { "keepAlive": true, "keepAliveMsecs": 15000, "path": null }, "protocol": "https:", "requests": {}, "scheduling": "lifo", "sockets": {}, "totalSocketCount": 0 }, "headers": { "Authorization": "Bearer REDACTED", "Content-Type": "text/json", "User-Agent": "vscode-extension/0.0.1 databricks-sdk-js/0.0.1 nodejs/16.14.2 os/darwin" }, "method": "GET" }, "timestamp": "1664462278553" }, { "level": "debug", "message": "REDACTED/api/2.0/preview/scim/v2/Me", "operationId": "b376e4c8-9140-49f5-873f-53dc4311c626", "operationName": "ApiClient.request", "response": { "active": true, "emails": [ { "primary": true, "type": "work", "value": "serge.smertin@databricks.com" } ], "externalId": "471d1718-d938-4b13-95b8-fd1d4e48894b", "groups": [ { "$ref": "Groups/737310559950744", "display": "admins", "type": "direct", "value": "737310559950744" } ], "id": "4183391249163402", "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:workspace:2.0:User:permissionLevel" ], "userName": "serge.smertin@databricks.com" }, "timestamp": "1664462279368" } ] ```
1 parent e8207e8 commit e6eeba1

21 files changed

Lines changed: 604 additions & 19 deletions

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"test": "yarn workspaces foreach run test",
1717
"test:integ": "yarn workspaces foreach run test:integ",
1818
"build": "yarn workspaces foreach run build",
19-
"clean": "yarn workspaces foreach run clean"
19+
"clean": "yarn workspaces foreach run clean",
20+
"fix": "yarn workspaces foreach run fix"
2021
},
2122
"repository": {
2223
"type": "git",

packages/databricks-sdk-js/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
},
3131
"dependencies": {
3232
"ini": "^3.0.1",
33-
"node-fetch": "^3.2.10"
33+
"inversify": "^6.0.1",
34+
"node-fetch": "^3.2.10",
35+
"reflect-metadata": "^0.1.13"
3436
},
3537
"devDependencies": {
3638
"@istanbuljs/nyc-config-typescript": "^1.0.2",
@@ -39,7 +41,7 @@
3941
"@types/chai-spies": "^1.0.3",
4042
"@types/ini": "^1.3.31",
4143
"@types/mocha": "^9.1.1",
42-
"@types/node": "^18.7.17",
44+
"@types/node": "^18.8.2",
4345
"@types/tmp": "^0.2.3",
4446
"@types/uuid": "^8.3.4",
4547
"chai": "^4.3.6",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import assert from "assert";
2+
import {defaultRedactor} from ".";
3+
import {onlyNBytes} from "./Redactor";
4+
5+
describe(__filename, () => {
6+
it("should redact by field names", () => {
7+
const testObj = {
8+
prop: "value1",
9+
nested: {
10+
prop: false,
11+
prop2: "value",
12+
headers: {
13+
header1: "test",
14+
header2: "value",
15+
},
16+
},
17+
};
18+
defaultRedactor.addFieldName("prop");
19+
const actual = defaultRedactor.sanitize(testObj, ["headers"]);
20+
const expected = {
21+
prop: "***REDACTED***",
22+
nested: {
23+
prop: "***REDACTED***",
24+
prop2: "value",
25+
},
26+
};
27+
assert.deepEqual(actual, expected);
28+
});
29+
30+
it("should truncate string to n bytes", () => {
31+
const n = 5;
32+
const str = "1234567890";
33+
assert.equal(onlyNBytes(str, n), "12345...(5 more bytes)");
34+
});
35+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
export function onlyNBytes(str: string, numBytes: number) {
2+
return str.length > numBytes
3+
? str.slice(0, numBytes) + `...(${str.length - numBytes} more bytes)`
4+
: str;
5+
}
6+
7+
function isPrimitveType(obj: any) {
8+
return Object(obj) !== obj;
9+
}
10+
11+
export class Redactor {
12+
constructor(private fieldNames: string[] = []) {}
13+
14+
addFieldName(fieldName: string) {
15+
this.fieldNames.push(fieldName);
16+
}
17+
18+
sanitize(
19+
obj: any,
20+
dropFields: string[] = [],
21+
maxFieldLength: number = 96
22+
): any {
23+
if (isPrimitveType(obj)) {
24+
if (typeof obj === "string") {
25+
return onlyNBytes(obj, maxFieldLength);
26+
}
27+
if (obj instanceof String) {
28+
return onlyNBytes(obj.toString(), maxFieldLength);
29+
}
30+
return obj;
31+
}
32+
33+
if (Array.isArray(obj)) {
34+
return obj.map((e) => this.sanitize(e, dropFields, maxFieldLength));
35+
}
36+
37+
//make a copy of the object
38+
obj = JSON.parse(JSON.stringify(obj));
39+
for (let key in obj) {
40+
if (dropFields.includes(key)) {
41+
delete obj[key];
42+
} else if (
43+
isPrimitveType(obj[key]) &&
44+
this.fieldNames.includes(key)
45+
) {
46+
obj[key] = "***REDACTED***";
47+
} else {
48+
obj[key] = this.sanitize(obj[key], dropFields, maxFieldLength);
49+
}
50+
}
51+
52+
return obj;
53+
}
54+
}
55+
56+
export const defaultRedactor = new Redactor([
57+
"string_value",
58+
"token_value",
59+
"content",
60+
]);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/naming-convention */
2-
2+
import ".";
33
import assert from "node:assert";
44
import {ApiClient} from "./api-client";
55

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ 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";
67
import {CancellationToken} from "./types";
78

89
const sdkVersion = require("../package.json").version;
@@ -51,11 +52,14 @@ export class ApiClient {
5152
return pairs.join(" ");
5253
}
5354

55+
@withLogContext("SDK")
5456
async request(
5557
path: string,
5658
method: HttpMethod,
5759
payload?: any,
58-
cancellationToken?: CancellationToken
60+
cancellationToken?: CancellationToken,
61+
@logOpId() opId?: string,
62+
@loggerInstance() logger?: NamedLogger
5963
): Promise<Object> {
6064
const credentials = await this.credentialProvider();
6165
const headers = {
@@ -84,6 +88,7 @@ export class ApiClient {
8488
let response;
8589

8690
try {
91+
logger?.debug(url.toString(), {request: options});
8792
const {abort, response: responsePromise} = await fetch(
8893
url.toString(),
8994
options
@@ -119,6 +124,7 @@ export class ApiClient {
119124

120125
try {
121126
response = JSON.parse(responseText);
127+
logger?.debug(url.toString(), {response: response});
122128
} catch (e) {
123129
throw new ApiClientResponseError(responseText, response);
124130
}

packages/databricks-sdk-js/src/auth/configFile.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from "node:path";
22
import {readFile, stat} from "node:fs/promises";
33
import {parse} from "ini";
44
import {homedir} from "node:os";
5+
import {defaultRedactor} from "../Redactor";
56

67
export type Profiles = Record<
78
string,
@@ -25,6 +26,13 @@ export function resolveConfigFilePath(filePath?: string): string {
2526
return filePath;
2627
}
2728

29+
function getProfile(config: any) {
30+
return {
31+
host: new URL(config.host),
32+
token: config.token,
33+
};
34+
}
35+
2836
export async function loadConfigFile(filePath?: string): Promise<Profiles> {
2937
filePath = resolveConfigFilePath(filePath);
3038

@@ -58,16 +66,10 @@ export async function loadConfigFile(filePath?: string): Promise<Profiles> {
5866
defaultSectionFound = true;
5967
continue;
6068
}
61-
profiles[key] = {
62-
host: new URL(config[key].host),
63-
token: config[key].token,
64-
};
69+
profiles[key] = getProfile(config[key]);
6570
}
6671
if (defaultSectionFound) {
67-
profiles["DEFAULT"] = {
68-
host: new URL(defaultSection.host),
69-
token: defaultSection.token,
70-
};
72+
profiles["DEFAULT"] = getProfile(defaultSection);
7173
}
7274
} catch (e: unknown) {
7375
let message;

packages/databricks-sdk-js/src/decorators.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/naming-convention */
2-
2+
import ".";
33
import assert from "node:assert";
44
import {ListRequest, ListReposResponse} from "./apis/repos";
55
import {paginated} from "./decorators";

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import "reflect-metadata";
2+
13
export * from "./api-client";
24

35
export * as cluster from "./apis/clusters";
@@ -40,3 +42,7 @@ export {ClusterFixture, TokenFixture} from "./test/fixtures";
4042

4143
export {RetryConfigs, default as retry} from "./retries/retries";
4244
export {TimeUnits, default as Time} from "./retries/Time";
45+
46+
export * as logging from "./logging";
47+
48+
export {Redactor, defaultRedactor} from "./Redactor";
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {Writable} from "stream";
2+
import {LogEntry, Logger} from "./types";
3+
4+
export class DefaultLogger implements Logger {
5+
private _stream: Writable;
6+
constructor(outputStream?: Writable) {
7+
this._stream = outputStream ?? process.stderr;
8+
}
9+
10+
log(level: string, message?: string, obj?: any) {
11+
this._stream.write(
12+
JSON.stringify({
13+
level: level,
14+
message: message,
15+
...obj,
16+
} as LogEntry)
17+
);
18+
}
19+
}

0 commit comments

Comments
 (0)