Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
db640fa
Add telemetry infrastructure: CircuitBreaker and FeatureFlagCache
samikshya-db Jan 28, 2026
211f91c
Add authentication support for REST API calls
samikshya-db Jan 29, 2026
c3779af
Fix feature flag and telemetry export endpoints
samikshya-db Jan 29, 2026
a105777
Match JDBC telemetry payload format
samikshya-db Jan 29, 2026
1cdb716
Fix lint errors
samikshya-db Jan 29, 2026
689e561
Add missing getAuthHeaders method to ClientContextStub
samikshya-db Jan 29, 2026
2d41e2d
Fix prettier formatting
samikshya-db Jan 29, 2026
e474256
Add DRIVER_NAME constant for nodejs-sql-driver
samikshya-db Jan 30, 2026
a3c9042
Add missing telemetry fields to match JDBC
samikshya-db Jan 30, 2026
6110797
Fix TypeScript compilation: add missing fields to system_configuratio…
samikshya-db Jan 30, 2026
42540bc
Fix telemetry PR review comments from #325
samikshya-db Feb 5, 2026
11590f3
Add proxy support to feature flag fetching
samikshya-db Feb 5, 2026
de17f1b
Merge latest main into telemetry-2-infrastructure
samikshya-db Mar 5, 2026
f531391
Merge branch 'main' into telemetry-2-infrastructure
samikshya-db Apr 2, 2026
79c1d31
Address PR #325 review feedback
samikshya-db Apr 10, 2026
adb5467
Merge branch 'main' into telemetry-2-infrastructure
samikshya-db Apr 13, 2026
12bc1ef
Merge branch 'main' into telemetry-2-infrastructure
samikshya-db Apr 13, 2026
71bce4f
refactor: replace node-fetch injection with sendRequest via connectio…
samikshya-db Apr 13, 2026
9a55d3b
style: fix prettier formatting in telemetry files
samikshya-db Apr 13, 2026
5dbc5a0
fix: address telemetry code review issues
samikshya-db Apr 14, 2026
5caae2b
fix: address code review findings for telemetry infrastructure
samikshya-db Apr 14, 2026
0201bda
style: fix prettier formatting in CircuitBreaker.ts
samikshya-db Apr 14, 2026
1205fff
fix: resolve ESLint errors in telemetry modules
samikshya-db Apr 15, 2026
00a62e8
fix: harden telemetry security + address code-review-squad findings
samikshya-db Apr 17, 2026
7ba52a5
test: document synthetic-JWT source pattern in redactSensitive test
samikshya-db Apr 17, 2026
2b516e9
fix: stringify sinon.Call.args[1] before regex test
samikshya-db Apr 17, 2026
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
17 changes: 17 additions & 0 deletions lib/DBSQLClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I
this.config.enableMetricViewMetadata = options.enableMetricViewMetadata;
}

// Persist userAgentEntry so telemetry and feature-flag call sites reuse
// the same value as the primary Thrift connection's User-Agent.
if (options.userAgentEntry !== undefined) {
this.config.userAgentEntry = options.userAgentEntry;
}

this.authProvider = this.createAuthProvider(options, authProvider);

this.connectionProvider = this.createConnectionProvider(options);
Expand Down Expand Up @@ -352,4 +358,15 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I
public async getDriver(): Promise<IDriver> {
return this.driver;
}

/**
* Returns the authentication provider associated with this client, if any.
* Intended for internal telemetry/feature-flag call sites that need to
* obtain auth headers directly without routing through `IClientContext`.
*
* @internal Not part of the public API. May change without notice.
*/
public getAuthProvider(): IAuthentication | undefined {
return this.authProvider;
}
}
7 changes: 7 additions & 0 deletions lib/contracts/IClientContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@ export interface ClientConfig {
telemetryBatchSize?: number;
telemetryFlushIntervalMs?: number;
telemetryMaxRetries?: number;
telemetryBackoffBaseMs?: number;
telemetryBackoffMaxMs?: number;
telemetryBackoffJitterMs?: number;
telemetryAuthenticatedExport?: boolean;
telemetryCircuitBreakerThreshold?: number;
telemetryCircuitBreakerTimeout?: number;
telemetryMaxPendingMetrics?: number;
telemetryMaxErrorsPerStatement?: number;
telemetryStatementTtlMs?: number;
userAgentEntry?: string;
}

export default interface IClientContext {
Expand Down
5 changes: 5 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import { LogLevel } from './contracts/IDBSQLLogger';
// Re-export types for TypeScript users
export type { default as ITokenProvider } from './connection/auth/tokenProvider/ITokenProvider';

// Re-export telemetry error classes so consumers can instanceof-check rather
// than string-matching error messages.
export { CircuitBreakerOpenError, CIRCUIT_BREAKER_OPEN_CODE } from './telemetry/CircuitBreaker';
export { TelemetryTerminalError } from './telemetry/DatabricksTelemetryExporter';

export const auth = {
PlainHttpAuthentication,
// Token provider classes for custom authentication
Expand Down
216 changes: 216 additions & 0 deletions lib/telemetry/CircuitBreaker.ts
Comment thread
samikshya-db marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/**
Comment thread
jadewang-db marked this conversation as resolved.
* Copyright (c) 2025 Databricks Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import IClientContext from '../contracts/IClientContext';
import { LogLevel } from '../contracts/IDBSQLLogger';

export enum CircuitBreakerState {
CLOSED = 'CLOSED',
OPEN = 'OPEN',
HALF_OPEN = 'HALF_OPEN',
}

export interface CircuitBreakerConfig {
failureThreshold: number;
timeout: number;
successThreshold: number;
}

export const DEFAULT_CIRCUIT_BREAKER_CONFIG: Readonly<CircuitBreakerConfig> = Object.freeze({
failureThreshold: 5,
timeout: 60000,
successThreshold: 2,
});

export const CIRCUIT_BREAKER_OPEN_CODE = 'CIRCUIT_BREAKER_OPEN' as const;

/**
* Thrown when execute() is called while the breaker is OPEN or a HALF_OPEN
* probe is already in flight. Callers identify the condition via
* `instanceof CircuitBreakerOpenError` or `err.code === CIRCUIT_BREAKER_OPEN_CODE`
* rather than string-matching the message.
*/
export class CircuitBreakerOpenError extends Error {
readonly code = CIRCUIT_BREAKER_OPEN_CODE;

constructor(message = 'Circuit breaker OPEN') {
super(message);
this.name = 'CircuitBreakerOpenError';
}
}

export class CircuitBreaker {
private state: CircuitBreakerState = CircuitBreakerState.CLOSED;

private failureCount = 0;

private successCount = 0;

private nextAttempt?: Date;

private halfOpenInflight = 0;

private readonly config: CircuitBreakerConfig;

constructor(private context: IClientContext, config?: Partial<CircuitBreakerConfig>) {
this.config = {
...DEFAULT_CIRCUIT_BREAKER_CONFIG,
...config,
};
}

async execute<T>(operation: () => Promise<T>): Promise<T> {
const admitted = this.tryAdmit();
if (!admitted) {
throw new CircuitBreakerOpenError();
}

const { wasHalfOpenProbe } = admitted;

try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
} finally {
if (wasHalfOpenProbe && this.halfOpenInflight > 0) {
this.halfOpenInflight -= 1;
}
}
}

/**
* Synchronous admission check. Returning `null` means "reject". Returning
* an object means the caller is admitted; `wasHalfOpenProbe` indicates
* whether this admission consumed the single HALF_OPEN probe slot so the
* caller can decrement it in `finally`.
*
* Running this as a single synchronous block is what prevents the
* concurrent-probe race that existed in the previous implementation.
*/
private tryAdmit(): { wasHalfOpenProbe: boolean } | null {
const logger = this.context.getLogger();

if (this.state === CircuitBreakerState.OPEN) {
if (this.nextAttempt && Date.now() < this.nextAttempt.getTime()) {
return null;
}
this.state = CircuitBreakerState.HALF_OPEN;
this.successCount = 0;
this.halfOpenInflight = 0;
logger.log(LogLevel.debug, 'Circuit breaker transitioned to HALF_OPEN');
}

if (this.state === CircuitBreakerState.HALF_OPEN) {
if (this.halfOpenInflight > 0) {
return null;
}
this.halfOpenInflight += 1;
return { wasHalfOpenProbe: true };
}

return { wasHalfOpenProbe: false };
}

getState(): CircuitBreakerState {
return this.state;
}

getFailureCount(): number {
return this.failureCount;
}

getSuccessCount(): number {
return this.successCount;
}

private onSuccess(): void {
const logger = this.context.getLogger();

this.failureCount = 0;

if (this.state === CircuitBreakerState.HALF_OPEN) {
this.successCount += 1;
logger.log(
LogLevel.debug,
`Circuit breaker success in HALF_OPEN (${this.successCount}/${this.config.successThreshold})`,
);

if (this.successCount >= this.config.successThreshold) {
this.state = CircuitBreakerState.CLOSED;
this.successCount = 0;
this.nextAttempt = undefined;
logger.log(LogLevel.debug, 'Circuit breaker transitioned to CLOSED');
}
}
}

private onFailure(): void {
const logger = this.context.getLogger();

this.failureCount += 1;
this.successCount = 0;

logger.log(LogLevel.debug, `Circuit breaker failure (${this.failureCount}/${this.config.failureThreshold})`);

if (this.state === CircuitBreakerState.HALF_OPEN || this.failureCount >= this.config.failureThreshold) {
this.state = CircuitBreakerState.OPEN;
this.nextAttempt = new Date(Date.now() + this.config.timeout);
logger.log(
LogLevel.warn,
`Telemetry circuit breaker OPEN after ${this.failureCount} failures (will retry after ${this.config.timeout}ms)`,
);
}
}
}

export class CircuitBreakerRegistry {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[F11] CircuitBreakerRegistry never shrinks — per-host leak (Severity: Medium)

Only removeCircuitBreaker / clear shrink this.breakers; nothing invokes them on session close. A long-lived multi-tenant process leaks one entry per distinct host forever.

Suggested fix: Have DBSQLClient.close() call registry.removeCircuitBreaker(host); or put an LRU cap on the map; ensure the registry is client-scoped (not a module singleton).

Posted by code-review-squad • flagged by ops.

private breakers: Map<string, CircuitBreaker>;

constructor(private context: IClientContext) {
this.breakers = new Map();
}

getCircuitBreaker(host: string, config?: Partial<CircuitBreakerConfig>): CircuitBreaker {
let breaker = this.breakers.get(host);
if (!breaker) {
breaker = new CircuitBreaker(this.context, config);
this.breakers.set(host, breaker);
const logger = this.context.getLogger();
logger.log(LogLevel.debug, `Created circuit breaker for host: ${host}`);
} else if (config) {
const logger = this.context.getLogger();
logger.log(LogLevel.debug, `Circuit breaker for host ${host} already exists; provided config will be ignored`);
}
return breaker;
}

getAllBreakers(): Map<string, CircuitBreaker> {
return new Map(this.breakers);
}

removeCircuitBreaker(host: string): void {
this.breakers.delete(host);
const logger = this.context.getLogger();
logger.log(LogLevel.debug, `Removed circuit breaker for host: ${host}`);
}

clear(): void {
this.breakers.clear();
}
}
Loading
Loading