Skip to content

Commit 8d8e1a8

Browse files
committed
send telemetry to unauth endpoint in case of connection/auth errors
Signed-off-by: Sai Shree Pradhan <saishree.pradhan@databricks.com>
1 parent 6748c2c commit 8d8e1a8

File tree

4 files changed

+174
-18
lines changed

4 files changed

+174
-18
lines changed

src/databricks/sql/client.py

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
InterfaceError,
2222
NotSupportedError,
2323
ProgrammingError,
24+
AuthenticationError,
25+
ConnectionError,
2426
)
2527
from databricks.sql.thrift_api.TCLIService import ttypes
2628
from databricks.sql.thrift_backend import ThriftBackend
@@ -241,9 +243,18 @@ def read(self) -> Optional[OAuthToken]:
241243
self.disable_pandas = kwargs.get("_disable_pandas", False)
242244
self.lz4_compression = kwargs.get("enable_query_result_lz4_compression", True)
243245

244-
auth_provider = get_python_sql_connector_auth_provider(
245-
server_hostname, **kwargs
246-
)
246+
try:
247+
auth_provider = get_python_sql_connector_auth_provider(
248+
server_hostname, **kwargs
249+
)
250+
except Exception as e:
251+
raise AuthenticationError(
252+
message=f"Failed to create authentication provider: {str(e)}",
253+
host_url=server_hostname,
254+
http_path=http_path,
255+
port=self.port,
256+
original_exception=e
257+
) from e
247258

248259
self.server_telemetry_enabled = True
249260
self.client_telemetry_enabled = kwargs.get("enable_telemetry", False)
@@ -281,20 +292,31 @@ def read(self) -> Optional[OAuthToken]:
281292
tls_client_cert_key_password=kwargs.get("_tls_client_cert_key_password"),
282293
)
283294

284-
self.thrift_backend = ThriftBackend(
285-
self.host,
286-
self.port,
287-
http_path,
288-
(http_headers or []) + base_headers,
289-
auth_provider,
290-
ssl_options=self._ssl_options,
291-
_use_arrow_native_complex_types=_use_arrow_native_complex_types,
292-
**kwargs,
293-
)
295+
try:
296+
self.thrift_backend = ThriftBackend(
297+
self.host,
298+
self.port,
299+
http_path,
300+
(http_headers or []) + base_headers,
301+
auth_provider,
302+
ssl_options=self._ssl_options,
303+
_use_arrow_native_complex_types=_use_arrow_native_complex_types,
304+
**kwargs,
305+
)
306+
307+
self._open_session_resp = self.thrift_backend.open_session(
308+
session_configuration, catalog, schema
309+
)
310+
except Exception as e:
311+
raise ConnectionError(
312+
message=f"Failed to establish connection: {str(e)}",
313+
host_url=self.host,
314+
http_path=http_path,
315+
port=self.port,
316+
user_agent=useragent_header,
317+
original_exception=e
318+
) from e
294319

295-
self._open_session_resp = self.thrift_backend.open_session(
296-
session_configuration, catalog, schema
297-
)
298320
self._session_handle = self._open_session_resp.sessionHandle
299321
self.protocol_version = self.get_protocol_version(self._open_session_resp)
300322
self.use_cloud_fetch = kwargs.get("use_cloud_fetch", True)

src/databricks/sql/exc.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,26 @@ def __init__(
2222

2323
error_name = self.__class__.__name__
2424
if session_id_hex:
25+
# Normal case: we have a session, send to regular telemetry client
2526
telemetry_client = TelemetryClientFactory.get_telemetry_client(
2627
session_id_hex
2728
)
2829
telemetry_client.export_failure_log(error_name, self.message)
30+
elif isinstance(self, (ConnectionError, AuthenticationError)) and 'host_url' in self.context:
31+
# Connection error case: no session but we should still send telemetry
32+
self._send_connection_error_telemetry(error_name)
33+
34+
def _send_connection_error_telemetry(self, error_name):
35+
"""Send connection error telemetry to unauthenticated endpoint"""
36+
37+
TelemetryClientFactory.send_connection_error_telemetry(
38+
error_name=error_name,
39+
error_message=self.message or str(self),
40+
host_url=self.context['host_url'],
41+
http_path=self.context.get('http_path', ''),
42+
port=self.context.get('port', 443),
43+
user_agent=self.context.get('user_agent'),
44+
)
2945

3046
def __str__(self):
3147
return self.message
@@ -126,3 +142,42 @@ class SessionAlreadyClosedError(RequestError):
126142

127143
class CursorAlreadyClosedError(RequestError):
128144
"""Thrown if CancelOperation receives a code 404. ThriftBackend should gracefully proceed as this is expected."""
145+
146+
147+
class ConnectionError(OperationalError):
148+
"""Thrown when connection to Databricks fails during initial setup"""
149+
150+
def __init__(
151+
self,
152+
message=None,
153+
host_url=None,
154+
http_path=None,
155+
port=443,
156+
user_agent=None,
157+
original_exception=None,
158+
**kwargs
159+
):
160+
# Set up context for connection error telemetry
161+
context = kwargs.get('context', {})
162+
if host_url:
163+
context.update({
164+
'host_url': host_url,
165+
'http_path': http_path or '',
166+
'port': port,
167+
'user_agent': user_agent,
168+
'original_exception': str(original_exception) if original_exception else None,
169+
})
170+
171+
super().__init__(message=message, context=context, **kwargs)
172+
173+
174+
class AuthenticationError(ConnectionError):
175+
"""Thrown when authentication to Databricks fails"""
176+
177+
def __init__(self, message=None, auth_type=None, **kwargs):
178+
context = kwargs.get('context', {})
179+
if auth_type:
180+
context['auth_type'] = auth_type
181+
kwargs['context'] = context
182+
183+
super().__init__(message=message, **kwargs)

src/databricks/sql/telemetry/models/event.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,9 @@ class TelemetryEvent(JsonSerializableMixin):
149149
operation_latency_ms (Optional[int]): Operation latency in milliseconds
150150
"""
151151

152-
session_id: str
153152
system_configuration: DriverSystemConfiguration
154153
driver_connection_params: DriverConnectionParameters
154+
session_id: Optional[str] = None
155155
sql_statement_id: Optional[str] = None
156156
auth_type: Optional[str] = None
157157
vol_operation: Optional[DriverVolumeOperation] = None

src/databricks/sql/telemetry/telemetry_client.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
TelemetryEvent,
1010
DriverSystemConfiguration,
1111
DriverErrorInfo,
12+
DriverConnectionParameters,
13+
HostDetails,
1214
)
1315
from databricks.sql.telemetry.models.frontend_logs import (
1416
TelemetryFrontendLog,
1517
TelemetryClientContext,
1618
FrontendLogContext,
1719
FrontendLogEntry,
1820
)
19-
from databricks.sql.telemetry.models.enums import AuthMech, AuthFlow
21+
from databricks.sql.telemetry.models.enums import AuthMech, AuthFlow, DatabricksClientType
2022
from databricks.sql.auth.authenticators import (
2123
AccessTokenAuthProvider,
2224
DatabricksOAuthProvider,
@@ -434,3 +436,80 @@ def close(session_id_hex):
434436
TelemetryClientFactory._executor.shutdown(wait=True)
435437
TelemetryClientFactory._executor = None
436438
TelemetryClientFactory._initialized = False
439+
440+
@staticmethod
441+
def send_connection_error_telemetry(
442+
error_name: str,
443+
error_message: str,
444+
host_url: str,
445+
http_path: str,
446+
port: int = 443,
447+
user_agent: Optional[str] = None,
448+
):
449+
"""Send error telemetry when connection creation fails, without requiring a session"""
450+
try:
451+
logger.debug("Sending connection error telemetry for host: %s", host_url)
452+
453+
# Initialize factory if needed (with proper locking)
454+
with TelemetryClientFactory._lock:
455+
TelemetryClientFactory._initialize()
456+
457+
# Create driver connection params for the failed connection
458+
driver_connection_params = DriverConnectionParameters(
459+
http_path=http_path,
460+
mode=DatabricksClientType.THRIFT,
461+
host_info=HostDetails(host_url=host_url, port=port),
462+
)
463+
464+
error_info = DriverErrorInfo(
465+
error_name=error_name,
466+
stack_trace=error_message
467+
)
468+
469+
telemetry_frontend_log = TelemetryFrontendLog(
470+
frontend_log_event_id=str(uuid.uuid4()),
471+
context=FrontendLogContext(
472+
client_context=TelemetryClientContext(
473+
timestamp_millis=int(time.time() * 1000),
474+
user_agent=user_agent or "PyDatabricksSqlConnector",
475+
)
476+
),
477+
entry=FrontendLogEntry(
478+
sql_driver_log=TelemetryEvent(
479+
system_configuration=TelemetryHelper.get_driver_system_configuration(),
480+
driver_connection_params=driver_connection_params,
481+
error_info=error_info,
482+
)
483+
),
484+
)
485+
486+
# Send to unauthenticated endpoint since we don't have working auth
487+
request = {
488+
"uploadTime": int(time.time() * 1000),
489+
"items": [],
490+
"protoLogs": [telemetry_frontend_log.to_json()],
491+
}
492+
493+
url = f"https://{host_url}/telemetry-unauth"
494+
headers = {"Accept": "application/json", "Content-Type": "application/json"}
495+
496+
# Send synchronously for connection errors since we're probably about to exit
497+
try:
498+
response = requests.post(
499+
url,
500+
data=json.dumps(request),
501+
headers=headers,
502+
timeout=5,
503+
)
504+
if response.status_code == 200:
505+
logger.debug("Connection error telemetry sent successfully")
506+
else:
507+
logger.debug(
508+
"Connection error telemetry failed with status: %s",
509+
response.status_code,
510+
)
511+
except Exception as e:
512+
logger.debug("Failed to send connection error telemetry: %s", e)
513+
514+
except Exception as e:
515+
logger.debug("Failed to create connection error telemetry: %s", e)

0 commit comments

Comments
 (0)