forked from airbytehq/airbyte
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CDK: emit
AirbyteTraceMessage
with exception trace information (air…
- Loading branch information
1 parent
7024731
commit 73c7fad
Showing
17 changed files
with
320 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# | ||
# Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
# | ||
|
||
import logging | ||
import sys | ||
|
||
from airbyte_cdk.utils.traced_exception import AirbyteTracedException | ||
|
||
|
||
def init_uncaught_exception_handler(logger: logging.Logger) -> None: | ||
""" | ||
Handles uncaught exceptions by emitting an AirbyteTraceMessage and making sure they are not | ||
printed to the console without having secrets removed. | ||
""" | ||
|
||
def hook_fn(exception_type, exception_value, traceback_): | ||
# For developer ergonomics, we want to see the stack trace in the logs when we do a ctrl-c | ||
if issubclass(exception_type, KeyboardInterrupt): | ||
sys.__excepthook__(exception_type, exception_value, traceback_) | ||
return | ||
|
||
logger.fatal(exception_value, exc_info=exception_value) | ||
|
||
# emit an AirbyteTraceMessage for any exception that gets to this spot | ||
traced_exc = ( | ||
exception_value | ||
if issubclass(exception_type, AirbyteTracedException) | ||
else AirbyteTracedException.from_exception(exception_value) | ||
) | ||
|
||
traced_exc.emit_message() | ||
|
||
sys.excepthook = hook_fn |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# | ||
# Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
# | ||
|
||
import traceback | ||
from datetime import datetime | ||
|
||
from airbyte_cdk.models import AirbyteErrorTraceMessage, AirbyteMessage, AirbyteTraceMessage, FailureType, TraceType | ||
from airbyte_cdk.models import Type as MessageType | ||
from airbyte_cdk.utils.airbyte_secrets_utils import filter_secrets | ||
|
||
|
||
class AirbyteTracedException(Exception): | ||
""" | ||
An exception that should be emitted as an AirbyteTraceMessage | ||
""" | ||
|
||
def __init__( | ||
self, | ||
internal_message: str = None, | ||
message: str = None, | ||
failure_type: FailureType = FailureType.system_error, | ||
exception: BaseException = None, | ||
): | ||
""" | ||
:param internal_message: the internal error that caused the failure | ||
:param message: a user-friendly message that indicates the cause of the error | ||
:param failure_type: the type of error | ||
:param exception: the exception that caused the error, from which the stack trace should be retrieved | ||
""" | ||
self.internal_message = internal_message | ||
self.message = message | ||
self.failure_type = failure_type | ||
self._exception = exception | ||
super().__init__(internal_message) | ||
|
||
def as_airbyte_message(self) -> AirbyteMessage: | ||
""" | ||
Builds an AirbyteTraceMessage from the exception | ||
""" | ||
now_millis = datetime.now().timestamp() * 1000.0 | ||
|
||
trace_exc = self._exception or self | ||
stack_trace_str = "".join(traceback.TracebackException.from_exception(trace_exc).format()) | ||
|
||
trace_message = AirbyteTraceMessage( | ||
type=TraceType.ERROR, | ||
emitted_at=now_millis, | ||
error=AirbyteErrorTraceMessage( | ||
message=self.message or "Something went wrong in the connector. See the logs for more details.", | ||
internal_message=self.internal_message, | ||
failure_type=self.failure_type, | ||
stack_trace=stack_trace_str, | ||
), | ||
) | ||
|
||
return AirbyteMessage(type=MessageType.TRACE, trace=trace_message) | ||
|
||
def emit_message(self): | ||
""" | ||
Prints the exception as an AirbyteTraceMessage. | ||
Note that this will be called automatically on uncaught exceptions when using the airbyte_cdk entrypoint. | ||
""" | ||
message = self.as_airbyte_message().json(exclude_unset=True) | ||
filtered_message = filter_secrets(message) | ||
print(filtered_message) | ||
|
||
@classmethod | ||
def from_exception(cls, exc: Exception, *args, **kwargs) -> "AirbyteTracedException": | ||
""" | ||
Helper to create an AirbyteTracedException from an existing exception | ||
:param exc: the exception that caused the error | ||
""" | ||
return cls(internal_message=str(exc), exception=exc, *args, **kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# | ||
# Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
# | ||
|
||
|
||
import json | ||
import subprocess | ||
import sys | ||
|
||
import pytest | ||
from airbyte_cdk.models import AirbyteErrorTraceMessage, AirbyteLogMessage, AirbyteMessage, AirbyteTraceMessage | ||
|
||
|
||
def test_uncaught_exception_handler(): | ||
cmd = "from airbyte_cdk.logger import init_logger; from airbyte_cdk.exception_handler import init_uncaught_exception_handler; logger = init_logger('airbyte'); init_uncaught_exception_handler(logger); raise 1" | ||
exception_message = "exceptions must derive from BaseException" | ||
exception_trace = ( | ||
"Traceback (most recent call last):\n" | ||
' File "<string>", line 1, in <module>\n' | ||
"TypeError: exceptions must derive from BaseException" | ||
) | ||
|
||
expected_log_message = AirbyteMessage( | ||
type="LOG", log=AirbyteLogMessage(level="FATAL", message=f"{exception_message}\n{exception_trace}") | ||
) | ||
|
||
expected_trace_message = AirbyteMessage( | ||
type="TRACE", | ||
trace=AirbyteTraceMessage( | ||
type="ERROR", | ||
emitted_at=0.0, | ||
error=AirbyteErrorTraceMessage( | ||
failure_type="system_error", | ||
message="Something went wrong in the connector. See the logs for more details.", | ||
internal_message=exception_message, | ||
stack_trace=f"{exception_trace}\n", | ||
), | ||
), | ||
) | ||
|
||
with pytest.raises(subprocess.CalledProcessError) as err: | ||
subprocess.check_output([sys.executable, "-c", cmd], stderr=subprocess.STDOUT) | ||
|
||
assert not err.value.stderr, "nothing on the stderr" | ||
|
||
stdout_lines = err.value.output.decode("utf-8").strip().split("\n") | ||
assert len(stdout_lines) == 2 | ||
|
||
log_output, trace_output = stdout_lines | ||
|
||
out_log_message = AirbyteMessage.parse_obj(json.loads(log_output)) | ||
assert out_log_message == expected_log_message, "Log message should be emitted in expected form" | ||
|
||
out_trace_message = AirbyteMessage.parse_obj(json.loads(trace_output)) | ||
assert out_trace_message.trace.emitted_at > 0 | ||
out_trace_message.trace.emitted_at = 0.0 # set a specific emitted_at value for testing | ||
assert out_trace_message == expected_trace_message, "Trace message should be emitted in expected form" |
Oops, something went wrong.