Skip to content

Commit 1bab5a2

Browse files
committed
Forward kernel telemetry options
1 parent 9460336 commit 1bab5a2

7 files changed

Lines changed: 274 additions & 5 deletions

File tree

‎lib/DBSQLClient.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,8 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I
729729
// doesn't ship in the public `.d.ts`. Mirrors Python's `kwargs.get("use_kernel")`
730730
// pattern (see databricks-sql-python/src/databricks/sql/session.py).
731731
const internalOptions = options as ConnectionOptions & InternalConnectionOptions;
732-
const backend = internalOptions.useKernel
732+
const useKernel = internalOptions.useKernel === true;
733+
const backend = useKernel
733734
? new KernelBackend({ context: this })
734735
: new ThriftBackend({
735736
context: this,
@@ -777,7 +778,7 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I
777778
`Telemetry remains controlled by the runtime config and feature flag.`,
778779
);
779780
}
780-
if (this.config.telemetryEnabled && !envDisabled) {
781+
if (!useKernel && this.config.telemetryEnabled && !envDisabled) {
781782
await this.initializeTelemetry();
782783
}
783784

‎lib/kernel/KernelAuth.ts‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import os from 'os';
1516
import { ConnectionOptions } from '../contracts/IDBSQLClient';
17+
import { ClientConfig } from '../contracts/IClientContext';
1618
import { InternalConnectionOptions } from '../contracts/InternalConnectionOptions';
1719
import AuthenticationError from '../errors/AuthenticationError';
1820
import HiveDriverError from '../errors/HiveDriverError';
1921
import { buildUserAgentString, normalizePemBytes } from '../utils';
22+
import driverVersion from '../version';
23+
import { DRIVER_NAME } from '../telemetry/types';
24+
import { sanitizeProcessName } from '../telemetry/telemetryUtils';
2025

2126
/**
2227
* Default local listener port for the U2M authorization-code callback.
@@ -131,6 +136,32 @@ export interface KernelSessionDefaults {
131136
retryOverallTimeoutSecs?: number;
132137
}
133138

139+
export interface KernelTelemetryOptions {
140+
/** Driver/runtime identity forwarded to kernel-owned telemetry. */
141+
driverName?: string;
142+
driverVersion?: string;
143+
runtimeName?: string;
144+
runtimeVersion?: string;
145+
runtimeVendor?: string;
146+
osName?: string;
147+
osVersion?: string;
148+
osArch?: string;
149+
clientAppName?: string;
150+
localeName?: string;
151+
charSetEncoding?: string;
152+
processName?: string;
153+
/** Kernel-owned telemetry switch and batching. */
154+
telemetryEnabled?: boolean;
155+
telemetryBatchSize?: number;
156+
telemetryFlushIntervalMs?: number;
157+
telemetryMaxRetries?: number;
158+
telemetryRetryDelayMs?: number;
159+
telemetryCloseFlushTimeoutMs?: number;
160+
telemetryCircuitBreakerEnabled?: boolean;
161+
telemetryCircuitBreakerThreshold?: number;
162+
telemetryCircuitBreakerTimeoutMs?: number;
163+
}
164+
134165
/**
135166
* TLS options shared across all auth-mode variants. Mirror the napi
136167
* binding's `ConnectionOptions.checkServerCertificate` / `.customCaCert`
@@ -227,6 +258,7 @@ export interface KernelFederationOptions {
227258
export type KernelNativeConnectionOptions = KernelSessionDefaults &
228259
KernelTlsOptions &
229260
KernelHttpOptions &
261+
KernelTelemetryOptions &
230262
KernelProxyOptions &
231263
KernelFederationOptions &
232264
(
@@ -588,6 +620,91 @@ export function buildKernelRetryOptions(config: {
588620
return out;
589621
}
590622

623+
function getLocaleName(env: NodeJS.ProcessEnv = process.env): string {
624+
try {
625+
const lang = env.LANG || env.LC_ALL || env.LC_MESSAGES || '';
626+
const match = lang.match(/^([a-z]{2}_[A-Z]{2})/);
627+
return match?.[1] ?? 'en_US';
628+
} catch {
629+
return 'en_US';
630+
}
631+
}
632+
633+
function getProcessName(): string {
634+
try {
635+
if (process.title && process.title !== 'node') {
636+
return sanitizeProcessName(process.title) || 'node';
637+
}
638+
const scriptPath = process.argv?.[1];
639+
if (scriptPath) {
640+
return sanitizeProcessName(scriptPath).replace(/\.[^.]*$/, '') || 'node';
641+
}
642+
return 'node';
643+
} catch {
644+
return 'node';
645+
}
646+
}
647+
648+
export function isTelemetryDisabledByEnv(env: NodeJS.ProcessEnv = process.env): boolean {
649+
const raw = env.DATABRICKS_TELEMETRY_DISABLED;
650+
const trimmed = typeof raw === 'string' ? raw.trim() : '';
651+
return trimmed.length > 0 && /^(1|true|yes|on)$/i.test(trimmed);
652+
}
653+
654+
export function buildKernelTelemetryOptions(
655+
config: Pick<
656+
ClientConfig,
657+
| 'telemetryEnabled'
658+
| 'telemetryBatchSize'
659+
| 'telemetryFlushIntervalMs'
660+
| 'telemetryMaxRetries'
661+
| 'telemetryBackoffBaseMs'
662+
| 'telemetryCloseTimeoutMs'
663+
| 'telemetryCircuitBreakerThreshold'
664+
| 'telemetryCircuitBreakerTimeout'
665+
>,
666+
) {
667+
const telemetry: KernelTelemetryOptions = {
668+
driverName: DRIVER_NAME,
669+
driverVersion,
670+
runtimeName: 'Node.js',
671+
runtimeVersion: process.version,
672+
runtimeVendor: 'Node.js Foundation',
673+
osName: process.platform,
674+
osVersion: os.release(),
675+
osArch: os.arch(),
676+
clientAppName: undefined,
677+
localeName: getLocaleName(),
678+
charSetEncoding: 'UTF-8',
679+
processName: getProcessName(),
680+
telemetryEnabled: (config.telemetryEnabled ?? true) && !isTelemetryDisabledByEnv(),
681+
};
682+
683+
if (Number.isFinite(config.telemetryBatchSize)) {
684+
telemetry.telemetryBatchSize = config.telemetryBatchSize;
685+
}
686+
if (Number.isFinite(config.telemetryFlushIntervalMs)) {
687+
telemetry.telemetryFlushIntervalMs = config.telemetryFlushIntervalMs;
688+
}
689+
if (Number.isFinite(config.telemetryMaxRetries)) {
690+
telemetry.telemetryMaxRetries = config.telemetryMaxRetries;
691+
}
692+
if (Number.isFinite(config.telemetryBackoffBaseMs)) {
693+
telemetry.telemetryRetryDelayMs = config.telemetryBackoffBaseMs;
694+
}
695+
if (Number.isFinite(config.telemetryCloseTimeoutMs)) {
696+
telemetry.telemetryCloseFlushTimeoutMs = config.telemetryCloseTimeoutMs;
697+
}
698+
if (Number.isFinite(config.telemetryCircuitBreakerThreshold)) {
699+
telemetry.telemetryCircuitBreakerThreshold = config.telemetryCircuitBreakerThreshold;
700+
}
701+
if (Number.isFinite(config.telemetryCircuitBreakerTimeout)) {
702+
telemetry.telemetryCircuitBreakerTimeoutMs = config.telemetryCircuitBreakerTimeout;
703+
}
704+
705+
return telemetry;
706+
}
707+
591708
/**
592709
* Map the public `ConnectionOptions.proxy` (`{protocol, host, port, auth}` —
593710
* the same shape the Thrift backend accepts) onto the kernel's structured napi

‎lib/kernel/KernelBackend.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ import HiveDriverError from '../errors/HiveDriverError';
2222
import { serializeQueryTags } from '../utils';
2323
import { getKernelNative, KernelNativeBinding, KernelConnection } from './KernelNativeLoader';
2424
import { decodeNapiKernelError } from './KernelErrorMapping';
25-
import { buildKernelConnectionOptions, buildKernelRetryOptions, KernelNativeConnectionOptions } from './KernelAuth';
25+
import {
26+
buildKernelConnectionOptions,
27+
buildKernelRetryOptions,
28+
buildKernelTelemetryOptions,
29+
KernelNativeConnectionOptions,
30+
} from './KernelAuth';
2631
import { installKernelLogBridge } from './KernelLogging';
2732
import KernelSessionBackend from './KernelSessionBackend';
2833

@@ -93,6 +98,7 @@ export default class KernelBackend implements IBackend {
9398
this.nativeOptions = {
9499
...buildKernelConnectionOptions(options),
95100
...buildKernelRetryOptions(this.context.getConfig()),
101+
...buildKernelTelemetryOptions(this.context.getConfig()),
96102
};
97103

98104
// Bridge the Rust kernel's `tracing` logs into the SAME `DBSQLLogger` the

‎native/kernel/index.d.ts‎

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/unit/DBSQLClient.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fs from 'fs';
44
import DBSQLClient, { ThriftLibrary } from '../../lib/DBSQLClient';
55
import DBSQLSession from '../../lib/DBSQLSession';
66
import ThriftBackend from '../../lib/thrift-backend/ThriftBackend';
7+
import KernelBackend from '../../lib/kernel/KernelBackend';
78

89
import PlainHttpAuthentication from '../../lib/connection/auth/PlainHttpAuthentication';
910
import DatabricksOAuth from '../../lib/connection/auth/DatabricksOAuth';
@@ -957,6 +958,22 @@ describe('DBSQLClient telemetry paths', () => {
957958
.filter((c) => c.args[0] === LogLevel.warn && /DATABRICKS_TELEMETRY_DISABLED/.test(c.args[1] as string));
958959
expect(warnCalls.length).to.equal(0);
959960
});
961+
962+
it('does not initialize Node telemetry on the kernel path', async () => {
963+
delete process.env.DATABRICKS_TELEMETRY_DISABLED;
964+
const client = new DBSQLClient();
965+
const initStub = sinon.stub(client as any, 'initializeTelemetry').resolves();
966+
sinon.stub(KernelBackend.prototype, 'connect').resolves();
967+
sinon.stub(KernelBackend.prototype, 'close').resolves();
968+
969+
try {
970+
await client.connect({ ...connectOptions, telemetryEnabled: true, useKernel: true } as any);
971+
972+
expect(initStub.callCount).to.equal(0);
973+
} finally {
974+
await client.close();
975+
}
976+
});
960977
});
961978

962979
describe('extractWorkspaceId', () => {

‎tests/unit/kernel/_helpers/nativeOptions.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,31 @@ export default function expectNativeConnectionOptions(actual: unknown, expectedR
3333
const { customHeaders, ...rest } = actual as Record<string, unknown> & {
3434
customHeaders?: Array<{ name: string; value: string }>;
3535
};
36+
for (const key of [
37+
'driverName',
38+
'driverVersion',
39+
'runtimeName',
40+
'runtimeVersion',
41+
'runtimeVendor',
42+
'osName',
43+
'osVersion',
44+
'osArch',
45+
'clientAppName',
46+
'localeName',
47+
'charSetEncoding',
48+
'processName',
49+
'telemetryEnabled',
50+
'telemetryBatchSize',
51+
'telemetryFlushIntervalMs',
52+
'telemetryMaxRetries',
53+
'telemetryRetryDelayMs',
54+
'telemetryCloseFlushTimeoutMs',
55+
'telemetryCircuitBreakerEnabled',
56+
'telemetryCircuitBreakerThreshold',
57+
'telemetryCircuitBreakerTimeoutMs',
58+
]) {
59+
delete rest[key];
60+
}
3661
expect(rest).to.deep.equal(expectedRest);
3762
expect(customHeaders, 'customHeaders').to.be.an('array').with.lengthOf(1);
3863
expect(customHeaders?.[0].name).to.equal('User-Agent');

‎tests/unit/kernel/execution.test.ts‎

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,13 +418,13 @@ function makeBinding(connection: KernelConnection): KernelNativeBinding & {
418418
return Object.assign(binding, { openSessionStub });
419419
}
420420

421-
function makeContext(logger?: IDBSQLLogger): IClientContext {
421+
function makeContext(logger?: IDBSQLLogger, configOverrides: Partial<ClientConfig> = {}): IClientContext {
422422
const log: IDBSQLLogger = logger ?? {
423423
log(_level: LogLevel, _message: string): void {
424424
// no-op
425425
},
426426
};
427-
const config = {} as ClientConfig;
427+
const config = configOverrides as ClientConfig;
428428
return {
429429
getConfig: () => config,
430430
getLogger: () => log,
@@ -551,6 +551,95 @@ describe('KernelBackend', () => {
551551
});
552552
});
553553

554+
it('openSession() forwards kernel-owned telemetry config and runtime identity to napi binding', async () => {
555+
const savedEnv = process.env.DATABRICKS_TELEMETRY_DISABLED;
556+
delete process.env.DATABRICKS_TELEMETRY_DISABLED;
557+
558+
const connection = new FakeNativeConnection();
559+
const binding = makeBinding(connection);
560+
const backend = new KernelBackend({
561+
context: makeContext(undefined, {
562+
telemetryEnabled: false,
563+
telemetryBatchSize: 17,
564+
telemetryFlushIntervalMs: 1_000,
565+
telemetryMaxRetries: 2,
566+
telemetryBackoffBaseMs: 50,
567+
telemetryCloseTimeoutMs: 2_500,
568+
telemetryCircuitBreakerThreshold: 3,
569+
telemetryCircuitBreakerTimeout: 60_000,
570+
}),
571+
nativeBinding: binding,
572+
});
573+
574+
try {
575+
await backend.connect({
576+
host: 'workspace.example',
577+
path: '/sql/1.0/warehouses/xyz',
578+
token: 'dapi-token',
579+
} as ConnectionOptions);
580+
581+
await backend.openSession({});
582+
583+
const args = binding.openSessionStub.firstCall.args[0] as Record<string, unknown>;
584+
expect(args.driverName).to.equal('nodejs-sql-driver');
585+
expect(args.driverVersion).to.be.a('string').and.not.equal('');
586+
expect(args.runtimeName).to.equal('Node.js');
587+
expect(args.runtimeVersion).to.equal(process.version);
588+
expect(args.runtimeVendor).to.equal('Node.js Foundation');
589+
expect(args.osName).to.equal(process.platform);
590+
expect(args.osVersion).to.be.a('string').and.not.equal('');
591+
expect(args.osArch).to.be.a('string').and.not.equal('');
592+
expect(args.localeName).to.be.a('string').and.not.equal('');
593+
expect(args.charSetEncoding).to.equal('UTF-8');
594+
expect(args.processName).to.be.a('string').and.not.equal('');
595+
expect(args.telemetryEnabled).to.equal(false);
596+
expect(args.telemetryBatchSize).to.equal(17);
597+
expect(args.telemetryFlushIntervalMs).to.equal(1_000);
598+
expect(args.telemetryMaxRetries).to.equal(2);
599+
expect(args.telemetryRetryDelayMs).to.equal(50);
600+
expect(args.telemetryCloseFlushTimeoutMs).to.equal(2_500);
601+
expect(args.telemetryCircuitBreakerEnabled).to.equal(undefined);
602+
expect(args.telemetryCircuitBreakerThreshold).to.equal(3);
603+
expect(args.telemetryCircuitBreakerTimeoutMs).to.equal(60_000);
604+
} finally {
605+
if (savedEnv === undefined) {
606+
delete process.env.DATABRICKS_TELEMETRY_DISABLED;
607+
} else {
608+
process.env.DATABRICKS_TELEMETRY_DISABLED = savedEnv;
609+
}
610+
}
611+
});
612+
613+
it('openSession() forwards env-disabled kernel telemetry even when config enables telemetry', async () => {
614+
const savedEnv = process.env.DATABRICKS_TELEMETRY_DISABLED;
615+
process.env.DATABRICKS_TELEMETRY_DISABLED = 'true';
616+
617+
const connection = new FakeNativeConnection();
618+
const binding = makeBinding(connection);
619+
const backend = new KernelBackend({
620+
context: makeContext(undefined, { telemetryEnabled: true }),
621+
nativeBinding: binding,
622+
});
623+
624+
try {
625+
await backend.connect({
626+
host: 'workspace.example',
627+
path: '/sql/1.0/warehouses/xyz',
628+
token: 'dapi-token',
629+
} as ConnectionOptions);
630+
await backend.openSession({});
631+
632+
const args = binding.openSessionStub.firstCall.args[0] as { telemetryEnabled?: boolean };
633+
expect(args.telemetryEnabled).to.equal(false);
634+
} finally {
635+
if (savedEnv === undefined) {
636+
delete process.env.DATABRICKS_TELEMETRY_DISABLED;
637+
} else {
638+
process.env.DATABRICKS_TELEMETRY_DISABLED = savedEnv;
639+
}
640+
}
641+
});
642+
554643
it('openSession() serializes session-level queryTags into sessionConf.QUERY_TAGS', async () => {
555644
const connection = new FakeNativeConnection();
556645
const binding = makeBinding(connection);

0 commit comments

Comments
 (0)